canicode 0.3.1 → 0.3.3
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 +28 -9
- package/dist/cli/index.js +110 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.js +42 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +97 -14
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +273 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/logo.png" alt="CanICode" width="80">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">CanICode</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/canicode"><img src="https://img.shields.io/npm/v/canicode.svg" alt="npm version"></a>
|
|
9
|
+
<a href="https://github.com/let-sunny/canicode/actions/workflows/ci.yml"><img src="https://github.com/let-sunny/canicode/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
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
|
+
<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
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">Analyze Figma designs. Score how dev-friendly and AI-friendly they are. Get actionable issues before writing code.</p>
|
|
15
|
+
|
|
16
|
+
<p align="center"><strong><a href="https://let-sunny.github.io/canicode/">Try it in your browser</a></strong> — no install needed.</p>
|
|
4
17
|
|
|
5
18
|
```bash
|
|
6
19
|
npm install -g canicode
|
|
@@ -27,13 +40,17 @@ canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
|
27
40
|
|
|
28
41
|
Each issue is classified: **Blocking** > **Risk** > **Missing Info** > **Suggestion**.
|
|
29
42
|
|
|
30
|
-
Scores use density + diversity weighting per category, combined into an overall grade (A/B/C/D/F).
|
|
43
|
+
Scores use density + diversity weighting per category, combined into an overall grade (S/A+/A/B+/B/C+/C/D/F).
|
|
31
44
|
|
|
32
45
|
---
|
|
33
46
|
|
|
34
47
|
## Getting Started
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
Four ways to use CanICode. Pick one.
|
|
50
|
+
|
|
51
|
+
### Web (no install)
|
|
52
|
+
|
|
53
|
+
Go to **[let-sunny.github.io/canicode](https://let-sunny.github.io/canicode/)**, paste a Figma URL, and get results instantly in your browser.
|
|
37
54
|
|
|
38
55
|
### CLI (standalone)
|
|
39
56
|
|
|
@@ -50,6 +67,8 @@ To enable "Comment on Figma" buttons in reports, set your Figma token:
|
|
|
50
67
|
canicode init --token figd_xxxxxxxxxxxxx
|
|
51
68
|
```
|
|
52
69
|
|
|
70
|
+
> **Get your token:** Figma → Settings → Security → Personal access tokens → Generate new token
|
|
71
|
+
|
|
53
72
|
### MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
54
73
|
|
|
55
74
|
**Claude Code:**
|
|
@@ -156,7 +175,7 @@ See [`examples/custom-rules.json`](examples/custom-rules.json) | Run `canicode d
|
|
|
156
175
|
<details>
|
|
157
176
|
<summary><strong>Config Overrides</strong></summary>
|
|
158
177
|
|
|
159
|
-
Override
|
|
178
|
+
Override rule scores, severity, node exclusions, and global settings:
|
|
160
179
|
|
|
161
180
|
```bash
|
|
162
181
|
canicode analyze <url> --config ./my-config.json
|
|
@@ -164,8 +183,8 @@ canicode analyze <url> --config ./my-config.json
|
|
|
164
183
|
|
|
165
184
|
```json
|
|
166
185
|
{
|
|
186
|
+
"excludeNodeNames": ["chatbot", "ad-banner", "wip"],
|
|
167
187
|
"gridBase": 4,
|
|
168
|
-
"colorTolerance": 5,
|
|
169
188
|
"rules": {
|
|
170
189
|
"no-auto-layout": { "score": -15, "severity": "blocking" },
|
|
171
190
|
"default-name": { "enabled": false }
|
|
@@ -175,7 +194,7 @@ canicode analyze <url> --config ./my-config.json
|
|
|
175
194
|
|
|
176
195
|
| Option | Description |
|
|
177
196
|
|--------|-------------|
|
|
178
|
-
| `gridBase` | Spacing grid unit (default:
|
|
197
|
+
| `gridBase` | Spacing grid unit (default: 4) |
|
|
179
198
|
| `colorTolerance` | Color difference tolerance (default: 10) |
|
|
180
199
|
| `excludeNodeTypes` | Node types to skip |
|
|
181
200
|
| `excludeNodeNames` | Node name patterns to skip |
|
|
@@ -183,7 +202,7 @@ canicode analyze <url> --config ./my-config.json
|
|
|
183
202
|
| `rules.<id>.severity` | Override rule severity |
|
|
184
203
|
| `rules.<id>.enabled` | Enable/disable a rule |
|
|
185
204
|
|
|
186
|
-
See [`examples/config.json`](examples/config.json) | Run `canicode docs config`
|
|
205
|
+
See [`examples/config.json`](examples/config.json) | [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) | Run `canicode docs config`
|
|
187
206
|
|
|
188
207
|
</details>
|
|
189
208
|
|
|
@@ -197,7 +216,7 @@ Density Score = 100 - (weighted issue count / node count) × 100
|
|
|
197
216
|
Diversity Score = (1 - unique violated rules / total rules in category) × 100
|
|
198
217
|
```
|
|
199
218
|
|
|
200
|
-
Severity weights issues — a single blocking issue counts 3x more than a suggestion. Scores are calculated per category and combined into an overall grade (A/B/C/D/F).
|
|
219
|
+
Severity weights issues — a single blocking issue counts 3x more than a suggestion. Scores are calculated per category and combined into an overall grade (S/A+/A/B+/B/C+/C/D/F).
|
|
201
220
|
|
|
202
221
|
</details>
|
|
203
222
|
|
package/dist/cli/index.js
CHANGED
|
@@ -185,7 +185,7 @@ var RULE_CONFIGS = {
|
|
|
185
185
|
score: -2,
|
|
186
186
|
enabled: true,
|
|
187
187
|
options: {
|
|
188
|
-
gridBase:
|
|
188
|
+
gridBase: 4
|
|
189
189
|
}
|
|
190
190
|
},
|
|
191
191
|
"magic-number-spacing": {
|
|
@@ -193,7 +193,7 @@ var RULE_CONFIGS = {
|
|
|
193
193
|
score: -4,
|
|
194
194
|
enabled: true,
|
|
195
195
|
options: {
|
|
196
|
-
gridBase:
|
|
196
|
+
gridBase: 4
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
"raw-shadow": {
|
|
@@ -502,11 +502,15 @@ var RuleEngine = class {
|
|
|
502
502
|
enabledRuleIds;
|
|
503
503
|
disabledRuleIds;
|
|
504
504
|
targetNodeId;
|
|
505
|
+
excludeNamePattern;
|
|
506
|
+
excludeNodeTypes;
|
|
505
507
|
constructor(options = {}) {
|
|
506
508
|
this.configs = options.configs ?? RULE_CONFIGS;
|
|
507
509
|
this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
|
|
508
510
|
this.disabledRuleIds = new Set(options.disabledRules ?? []);
|
|
509
511
|
this.targetNodeId = options.targetNodeId;
|
|
512
|
+
this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
|
|
513
|
+
this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
|
|
510
514
|
}
|
|
511
515
|
/**
|
|
512
516
|
* Analyze a Figma file and return issues
|
|
@@ -560,6 +564,12 @@ var RuleEngine = class {
|
|
|
560
564
|
*/
|
|
561
565
|
traverseAndCheck(node, file, rules, maxDepth, issues, depth, path, parent, siblings) {
|
|
562
566
|
const nodePath = [...path, node.name];
|
|
567
|
+
if (this.excludeNodeTypes && this.excludeNodeTypes.has(node.type)) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
563
573
|
const context = {
|
|
564
574
|
file,
|
|
565
575
|
parent,
|
|
@@ -754,6 +764,12 @@ function transformNode(node) {
|
|
|
754
764
|
if ("layoutPositioning" in node && node.layoutPositioning) {
|
|
755
765
|
base.layoutPositioning = node.layoutPositioning;
|
|
756
766
|
}
|
|
767
|
+
if ("layoutSizingHorizontal" in node && node.layoutSizingHorizontal) {
|
|
768
|
+
base.layoutSizingHorizontal = node.layoutSizingHorizontal;
|
|
769
|
+
}
|
|
770
|
+
if ("layoutSizingVertical" in node && node.layoutSizingVertical) {
|
|
771
|
+
base.layoutSizingVertical = node.layoutSizingVertical;
|
|
772
|
+
}
|
|
757
773
|
if ("primaryAxisAlignItems" in node) {
|
|
758
774
|
base.primaryAxisAlignItems = node.primaryAxisAlignItems;
|
|
759
775
|
}
|
|
@@ -1479,7 +1495,7 @@ function generateHtmlReport(file, result, scores, options) {
|
|
|
1479
1495
|
${CATEGORIES.map((cat) => {
|
|
1480
1496
|
const cs = scores.byCategory[cat];
|
|
1481
1497
|
const desc = CATEGORY_DESCRIPTIONS[cat];
|
|
1482
|
-
return ` <
|
|
1498
|
+
return ` <a href="#cat-${cat}" class="flex flex-col items-center group relative cursor-pointer no-underline text-foreground hover:opacity-80 transition-opacity">
|
|
1483
1499
|
${renderGaugeSvg(cs.percentage, 100, 7)}
|
|
1484
1500
|
<span class="text-xs font-medium mt-2.5 text-center leading-tight">${CATEGORY_LABELS[cat]}</span>
|
|
1485
1501
|
<span class="text-[11px] text-muted-foreground">${cs.issueCount} issues</span>
|
|
@@ -1487,7 +1503,7 @@ ${CATEGORIES.map((cat) => {
|
|
|
1487
1503
|
${esc(desc)}
|
|
1488
1504
|
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div>
|
|
1489
1505
|
</div>
|
|
1490
|
-
</
|
|
1506
|
+
</a>`;
|
|
1491
1507
|
}).join("\n")}
|
|
1492
1508
|
</div>
|
|
1493
1509
|
</section>
|
|
@@ -1542,7 +1558,7 @@ ${figmaToken ? ` <script>
|
|
|
1542
1558
|
const res = await fetch('https://api.figma.com/v1/files/' + fileKey + '/comments', {
|
|
1543
1559
|
method: 'POST',
|
|
1544
1560
|
headers: { 'X-FIGMA-TOKEN': FIGMA_TOKEN, 'Content-Type': 'application/json' },
|
|
1545
|
-
body: JSON.stringify({ message: commentBody, client_meta: { node_id: nodeId } }),
|
|
1561
|
+
body: JSON.stringify({ message: commentBody, client_meta: { node_id: nodeId, node_offset: { x: 0, y: 0 } } }),
|
|
1546
1562
|
});
|
|
1547
1563
|
if (!res.ok) throw new Error(await res.text());
|
|
1548
1564
|
btn.textContent = 'Sent \\u2713';
|
|
@@ -1625,7 +1641,7 @@ function renderCategory(cat, scores, issues, fileKey, screenshotMap, figmaToken)
|
|
|
1625
1641
|
for (const sev of SEVERITY_ORDER) bySeverity.set(sev, []);
|
|
1626
1642
|
for (const issue of issues) bySeverity.get(issue.config.severity)?.push(issue);
|
|
1627
1643
|
return `
|
|
1628
|
-
<details class="bg-card border border-border rounded-lg shadow-sm overflow-hidden group"${hasProblems ? " open" : ""}>
|
|
1644
|
+
<details id="cat-${cat}" class="bg-card border border-border rounded-lg shadow-sm overflow-hidden group"${hasProblems ? " open" : ""}>
|
|
1629
1645
|
<summary class="px-5 py-3.5 flex items-center gap-3 cursor-pointer hover:bg-muted/50 transition-colors select-none">
|
|
1630
1646
|
<span class="inline-flex items-center justify-center w-10 h-6 rounded-md text-xs font-bold border ${scoreBadgeStyle(cs.percentage)}">${cs.percentage}</span>
|
|
1631
1647
|
<div class="flex-1 min-w-0">
|
|
@@ -2305,6 +2321,12 @@ var ActivityLogger = class {
|
|
|
2305
2321
|
}
|
|
2306
2322
|
};
|
|
2307
2323
|
|
|
2324
|
+
// src/rules/excluded-names.ts
|
|
2325
|
+
var EXCLUDED_NAME_PATTERN = /(badge|close|dismiss|overlay|float|fab|dot|indicator|corner|decoration|tag|status|notification|icon|ico|image|asset|filter|dim|dimmed|bg|background|logo|avatar|divider|separator|nav|navigation|gnb|header|footer|sidebar|toolbar|modal|dialog|popup|toast|tooltip|dropdown|menu|sticky|spinner|loader|cursor|cta|chatbot|thumb|thumbnail|tabbar|tab-bar|statusbar|status-bar)/i;
|
|
2326
|
+
function isExcludedName(name) {
|
|
2327
|
+
return EXCLUDED_NAME_PATTERN.test(name);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2308
2330
|
// src/agents/orchestrator.ts
|
|
2309
2331
|
function selectNodes(summaries, strategy, maxNodes) {
|
|
2310
2332
|
if (summaries.length === 0) return [];
|
|
@@ -2360,14 +2382,13 @@ var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2360
2382
|
"COMPONENT",
|
|
2361
2383
|
"INSTANCE"
|
|
2362
2384
|
]);
|
|
2363
|
-
var ICON_NAME_PATTERN = /\b(icon|ico|badge|indicator)\b/i;
|
|
2364
2385
|
function filterConversionCandidates(summaries, documentRoot) {
|
|
2365
2386
|
return summaries.filter((summary) => {
|
|
2366
2387
|
const node = findNode(documentRoot, summary.nodeId);
|
|
2367
2388
|
if (!node) return false;
|
|
2368
2389
|
if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
|
|
2369
2390
|
if (!ELIGIBLE_NODE_TYPES.has(node.type)) return false;
|
|
2370
|
-
if (
|
|
2391
|
+
if (isExcludedName(node.name)) return false;
|
|
2371
2392
|
const bbox = node.absoluteBoundingBox;
|
|
2372
2393
|
if (bbox && (bbox.width < MIN_WIDTH || bbox.height < MIN_HEIGHT)) return false;
|
|
2373
2394
|
if (!node.children || node.children.length < 3) return false;
|
|
@@ -2752,7 +2773,7 @@ Override canicode's default rule scores, severity, and filters.
|
|
|
2752
2773
|
STRUCTURE
|
|
2753
2774
|
- excludeNodeTypes: node types to skip (e.g. VECTOR, BOOLEAN_OPERATION)
|
|
2754
2775
|
- excludeNodeNames: name patterns to skip (e.g. icon, ico)
|
|
2755
|
-
- gridBase: spacing grid unit, default
|
|
2776
|
+
- gridBase: spacing grid unit, default 4
|
|
2756
2777
|
- colorTolerance: color diff tolerance, default 10
|
|
2757
2778
|
- rules: per-rule overrides (score, severity, enabled)
|
|
2758
2779
|
|
|
@@ -2770,7 +2791,9 @@ EXAMPLE
|
|
|
2770
2791
|
|
|
2771
2792
|
USAGE
|
|
2772
2793
|
canicode analyze <url> --config ./my-config.json
|
|
2773
|
-
|
|
2794
|
+
|
|
2795
|
+
Full guide: docs/CUSTOMIZATION.md
|
|
2796
|
+
Examples: examples/config.json
|
|
2774
2797
|
`.trimStart());
|
|
2775
2798
|
}
|
|
2776
2799
|
var DOCS_TOPICS = {
|
|
@@ -2836,7 +2859,6 @@ var absolutePositionInAutoLayoutDef = {
|
|
|
2836
2859
|
impact: "Element will not respond to sibling changes, may overlap unexpectedly",
|
|
2837
2860
|
fix: "Remove absolute positioning or use proper Auto Layout alignment"
|
|
2838
2861
|
};
|
|
2839
|
-
var INTENTIONAL_ABSOLUTE_PATTERNS = /^(badge|close|dismiss|overlay|float|fab|dot|indicator|corner|decoration|tag|status|notification|x|icon[-_ ]?(close|dismiss|x)|btn[-_ ]?(close|dismiss))/i;
|
|
2840
2862
|
function isSmallRelativeToParent(node, parent) {
|
|
2841
2863
|
const nodeBB = node.absoluteBoundingBox;
|
|
2842
2864
|
const parentBB = parent.absoluteBoundingBox;
|
|
@@ -2850,7 +2872,8 @@ var absolutePositionInAutoLayoutCheck = (node, context) => {
|
|
|
2850
2872
|
if (!context.parent) return null;
|
|
2851
2873
|
if (!hasAutoLayout(context.parent)) return null;
|
|
2852
2874
|
if (node.layoutPositioning !== "ABSOLUTE") return null;
|
|
2853
|
-
if (
|
|
2875
|
+
if (node.type === "VECTOR" || node.type === "BOOLEAN_OPERATION" || node.type === "LINE" || node.type === "ELLIPSE" || node.type === "STAR" || node.type === "REGULAR_POLYGON") return null;
|
|
2876
|
+
if (isExcludedName(node.name)) return null;
|
|
2854
2877
|
if (isSmallRelativeToParent(node, context.parent)) return null;
|
|
2855
2878
|
if (context.parent.type === "COMPONENT") return null;
|
|
2856
2879
|
return {
|
|
@@ -2876,10 +2899,14 @@ var fixedWidthInResponsiveContextCheck = (node, context) => {
|
|
|
2876
2899
|
if (!context.parent) return null;
|
|
2877
2900
|
if (!hasAutoLayout(context.parent)) return null;
|
|
2878
2901
|
if (!isContainerNode(node)) return null;
|
|
2879
|
-
if (node.
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2902
|
+
if (node.layoutSizingHorizontal) {
|
|
2903
|
+
if (node.layoutSizingHorizontal !== "FIXED") return null;
|
|
2904
|
+
} else {
|
|
2905
|
+
if (node.layoutAlign === "STRETCH") return null;
|
|
2906
|
+
if (!node.absoluteBoundingBox) return null;
|
|
2907
|
+
if (node.layoutAlign !== "INHERIT") return null;
|
|
2908
|
+
}
|
|
2909
|
+
if (isExcludedName(node.name)) return null;
|
|
2883
2910
|
return {
|
|
2884
2911
|
ruleId: fixedWidthInResponsiveContextDef.id,
|
|
2885
2912
|
nodeId: node.id,
|
|
@@ -3152,7 +3179,7 @@ var inconsistentSpacingDef = {
|
|
|
3152
3179
|
fix: "Use spacing values from the design system grid (e.g., 8pt increments)"
|
|
3153
3180
|
};
|
|
3154
3181
|
var inconsistentSpacingCheck = (node, context, options) => {
|
|
3155
|
-
const gridBase = options?.["gridBase"] ?? getRuleOption("inconsistent-spacing", "gridBase",
|
|
3182
|
+
const gridBase = options?.["gridBase"] ?? getRuleOption("inconsistent-spacing", "gridBase", 4);
|
|
3156
3183
|
const paddings = [
|
|
3157
3184
|
node.paddingLeft,
|
|
3158
3185
|
node.paddingRight,
|
|
@@ -3194,7 +3221,7 @@ var magicNumberSpacingDef = {
|
|
|
3194
3221
|
fix: "Round spacing to the nearest grid value or use spacing tokens"
|
|
3195
3222
|
};
|
|
3196
3223
|
var magicNumberSpacingCheck = (node, context, options) => {
|
|
3197
|
-
const gridBase = options?.["gridBase"] ?? getRuleOption("magic-number-spacing", "gridBase",
|
|
3224
|
+
const gridBase = options?.["gridBase"] ?? getRuleOption("magic-number-spacing", "gridBase", 4);
|
|
3198
3225
|
const allSpacings = [
|
|
3199
3226
|
node.paddingLeft,
|
|
3200
3227
|
node.paddingRight,
|
|
@@ -3511,6 +3538,7 @@ var defaultNameDef = {
|
|
|
3511
3538
|
};
|
|
3512
3539
|
var defaultNameCheck = (node, context) => {
|
|
3513
3540
|
if (!node.name) return null;
|
|
3541
|
+
if (isExcludedName(node.name)) return null;
|
|
3514
3542
|
if (!isDefaultName(node.name)) return null;
|
|
3515
3543
|
return {
|
|
3516
3544
|
ruleId: defaultNameDef.id,
|
|
@@ -3533,6 +3561,7 @@ var nonSemanticNameDef = {
|
|
|
3533
3561
|
};
|
|
3534
3562
|
var nonSemanticNameCheck = (node, context) => {
|
|
3535
3563
|
if (!node.name) return null;
|
|
3564
|
+
if (isExcludedName(node.name)) return null;
|
|
3536
3565
|
if (!isNonSemanticName(node.name)) return null;
|
|
3537
3566
|
if (!node.children || node.children.length === 0) {
|
|
3538
3567
|
const shapeTypes = ["RECTANGLE", "ELLIPSE", "VECTOR", "LINE", "STAR", "REGULAR_POLYGON"];
|
|
@@ -3601,6 +3630,7 @@ var numericSuffixNameDef = {
|
|
|
3601
3630
|
};
|
|
3602
3631
|
var numericSuffixNameCheck = (node, context) => {
|
|
3603
3632
|
if (!node.name) return null;
|
|
3633
|
+
if (isExcludedName(node.name)) return null;
|
|
3604
3634
|
if (isDefaultName(node.name)) return null;
|
|
3605
3635
|
if (!hasNumericSuffix(node.name)) return null;
|
|
3606
3636
|
return {
|
|
@@ -4040,9 +4070,13 @@ Warning: Analyzing ${totalNodes} nodes without scope. Results may be noisy.`);
|
|
|
4040
4070
|
Analyzing: ${file.name}`);
|
|
4041
4071
|
console.log(`Nodes: ${totalNodes}`);
|
|
4042
4072
|
let configs = options.preset ? { ...getConfigsWithPreset(options.preset) } : { ...RULE_CONFIGS };
|
|
4073
|
+
let excludeNodeNames;
|
|
4074
|
+
let excludeNodeTypes;
|
|
4043
4075
|
if (options.config) {
|
|
4044
4076
|
const configFile = await loadConfigFile(options.config);
|
|
4045
4077
|
configs = mergeConfigs(configs, configFile);
|
|
4078
|
+
excludeNodeNames = configFile.excludeNodeNames;
|
|
4079
|
+
excludeNodeTypes = configFile.excludeNodeTypes;
|
|
4046
4080
|
console.log(`Config loaded: ${options.config}`);
|
|
4047
4081
|
}
|
|
4048
4082
|
if (options.customRules) {
|
|
@@ -4055,7 +4089,9 @@ Analyzing: ${file.name}`);
|
|
|
4055
4089
|
}
|
|
4056
4090
|
const analyzeOptions = {
|
|
4057
4091
|
configs,
|
|
4058
|
-
...effectiveNodeId && { targetNodeId: effectiveNodeId }
|
|
4092
|
+
...effectiveNodeId && { targetNodeId: effectiveNodeId },
|
|
4093
|
+
...excludeNodeNames && { excludeNodeNames },
|
|
4094
|
+
...excludeNodeTypes && { excludeNodeTypes }
|
|
4059
4095
|
};
|
|
4060
4096
|
const result = analyzeFile(file, analyzeOptions);
|
|
4061
4097
|
console.log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
|
|
@@ -4346,6 +4382,61 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
|
|
|
4346
4382
|
process.exit(1);
|
|
4347
4383
|
}
|
|
4348
4384
|
});
|
|
4385
|
+
cli.command("list-rules", "List all analysis rules with scores and severity").option("--custom-rules <path>", "Include custom rules from JSON file").option("--config <path>", "Apply config overrides to show effective scores").option("--json", "Output as JSON").action(async (options) => {
|
|
4386
|
+
try {
|
|
4387
|
+
let configs = { ...RULE_CONFIGS };
|
|
4388
|
+
if (options.config) {
|
|
4389
|
+
const configFile = await loadConfigFile(options.config);
|
|
4390
|
+
configs = mergeConfigs(configs, configFile);
|
|
4391
|
+
}
|
|
4392
|
+
if (options.customRules) {
|
|
4393
|
+
const { rules: rules2, configs: customConfigs } = await loadCustomRules(options.customRules);
|
|
4394
|
+
for (const rule of rules2) {
|
|
4395
|
+
ruleRegistry.register(rule);
|
|
4396
|
+
}
|
|
4397
|
+
configs = { ...configs, ...customConfigs };
|
|
4398
|
+
}
|
|
4399
|
+
const rules = ruleRegistry.getAll().map((rule) => {
|
|
4400
|
+
const config2 = configs[rule.definition.id];
|
|
4401
|
+
return {
|
|
4402
|
+
id: rule.definition.id,
|
|
4403
|
+
name: rule.definition.name,
|
|
4404
|
+
category: rule.definition.category,
|
|
4405
|
+
severity: config2?.severity ?? "risk",
|
|
4406
|
+
score: config2?.score ?? 0,
|
|
4407
|
+
enabled: config2?.enabled ?? true
|
|
4408
|
+
};
|
|
4409
|
+
});
|
|
4410
|
+
if (options.json) {
|
|
4411
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
4415
|
+
for (const rule of rules) {
|
|
4416
|
+
const list = byCategory.get(rule.category) ?? [];
|
|
4417
|
+
list.push(rule);
|
|
4418
|
+
byCategory.set(rule.category, list);
|
|
4419
|
+
}
|
|
4420
|
+
for (const [category, catRules] of byCategory) {
|
|
4421
|
+
console.log(`
|
|
4422
|
+
${category.toUpperCase()}`);
|
|
4423
|
+
for (const r of catRules) {
|
|
4424
|
+
const status = r.enabled ? "" : " (disabled)";
|
|
4425
|
+
const pad = " ".repeat(Math.max(0, 40 - r.id.length));
|
|
4426
|
+
console.log(` ${r.id}${pad} ${String(r.score).padStart(4)} ${r.severity}${status}`);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
console.log(`
|
|
4430
|
+
Total: ${rules.length} rules
|
|
4431
|
+
`);
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
console.error(
|
|
4434
|
+
"\nError:",
|
|
4435
|
+
error instanceof Error ? error.message : String(error)
|
|
4436
|
+
);
|
|
4437
|
+
process.exit(1);
|
|
4438
|
+
}
|
|
4439
|
+
});
|
|
4349
4440
|
cli.command("docs [topic]", "Show documentation (topics: setup, rules, config)").action((topic) => {
|
|
4350
4441
|
handleDocs(topic);
|
|
4351
4442
|
});
|