canicode 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.js +379 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +32 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +338 -11
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +32 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,7 +194,7 @@ canicode analyze <url> --config ./my-config.json
|
|
|
194
194
|
|
|
195
195
|
| Option | Description |
|
|
196
196
|
|--------|-------------|
|
|
197
|
-
| `gridBase` | Spacing grid unit (default:
|
|
197
|
+
| `gridBase` | Spacing grid unit (default: 4) |
|
|
198
198
|
| `colorTolerance` | Color difference tolerance (default: 10) |
|
|
199
199
|
| `excludeNodeTypes` | Node types to skip |
|
|
200
200
|
| `excludeNodeNames` | Node name patterns to skip |
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,204 @@ import cac from 'cac';
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __esm = (fn, res) => function __init() {
|
|
13
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
+
};
|
|
15
|
+
var __export = (target, all) => {
|
|
16
|
+
for (var name in all)
|
|
17
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/monitoring/browser.ts
|
|
21
|
+
var browser_exports = {};
|
|
22
|
+
__export(browser_exports, {
|
|
23
|
+
initBrowserMonitoring: () => initBrowserMonitoring,
|
|
24
|
+
shutdownBrowserMonitoring: () => shutdownBrowserMonitoring,
|
|
25
|
+
trackBrowserError: () => trackBrowserError,
|
|
26
|
+
trackBrowserEvent: () => trackBrowserEvent
|
|
27
|
+
});
|
|
28
|
+
function getGlobal() {
|
|
29
|
+
return globalThis;
|
|
30
|
+
}
|
|
31
|
+
function injectScript(src) {
|
|
32
|
+
return new Promise((resolve7, reject) => {
|
|
33
|
+
const g = getGlobal();
|
|
34
|
+
const doc = g["document"];
|
|
35
|
+
if (!doc) {
|
|
36
|
+
resolve7();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const script = doc["createElement"]("script");
|
|
40
|
+
script["src"] = src;
|
|
41
|
+
script["async"] = true;
|
|
42
|
+
script["onload"] = () => resolve7();
|
|
43
|
+
script["onerror"] = () => reject(new Error(`Failed to load script: ${src}`));
|
|
44
|
+
doc["head"]["appendChild"](script);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function initBrowserMonitoring(config2) {
|
|
48
|
+
if (config2.enabled === false) return;
|
|
49
|
+
const g = getGlobal();
|
|
50
|
+
if (!g["document"]) return;
|
|
51
|
+
monitoringEnabled = true;
|
|
52
|
+
if (config2.posthogApiKey) {
|
|
53
|
+
try {
|
|
54
|
+
await injectScript("https://us-assets.i.posthog.com/static/array.js");
|
|
55
|
+
g["posthog"]?.["init"]?.(config2.posthogApiKey, {
|
|
56
|
+
api_host: "https://us.i.posthog.com",
|
|
57
|
+
autocapture: false,
|
|
58
|
+
capture_pageview: true
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (config2.sentryDsn) {
|
|
64
|
+
try {
|
|
65
|
+
await injectScript("https://browser.sentry-cdn.com/8.0.0/bundle.min.js");
|
|
66
|
+
g["Sentry"]?.["init"]?.({
|
|
67
|
+
dsn: config2.sentryDsn,
|
|
68
|
+
environment: config2.environment ?? "web",
|
|
69
|
+
release: config2.version,
|
|
70
|
+
tracesSampleRate: 0
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function trackBrowserEvent(event, properties) {
|
|
77
|
+
if (!monitoringEnabled) return;
|
|
78
|
+
try {
|
|
79
|
+
getGlobal()["posthog"]?.["capture"]?.(event, properties);
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function trackBrowserError(error, context) {
|
|
84
|
+
if (!monitoringEnabled) return;
|
|
85
|
+
try {
|
|
86
|
+
getGlobal()["Sentry"]?.["captureException"]?.(
|
|
87
|
+
error,
|
|
88
|
+
context ? { extra: context } : void 0
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
getGlobal()["posthog"]?.["capture"]?.("error", {
|
|
94
|
+
error: error.message,
|
|
95
|
+
...context
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function shutdownBrowserMonitoring() {
|
|
101
|
+
monitoringEnabled = false;
|
|
102
|
+
}
|
|
103
|
+
var monitoringEnabled;
|
|
104
|
+
var init_browser = __esm({
|
|
105
|
+
"src/monitoring/browser.ts"() {
|
|
106
|
+
monitoringEnabled = false;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// src/monitoring/node.ts
|
|
111
|
+
var node_exports = {};
|
|
112
|
+
__export(node_exports, {
|
|
113
|
+
initNodeMonitoring: () => initNodeMonitoring,
|
|
114
|
+
shutdownNodeMonitoring: () => shutdownNodeMonitoring,
|
|
115
|
+
trackNodeError: () => trackNodeError,
|
|
116
|
+
trackNodeEvent: () => trackNodeEvent
|
|
117
|
+
});
|
|
118
|
+
async function initNodeMonitoring(config2) {
|
|
119
|
+
if (config2.enabled === false) return;
|
|
120
|
+
monitoringEnabled2 = true;
|
|
121
|
+
commonProps = {
|
|
122
|
+
_sdk: "canicode",
|
|
123
|
+
_sdk_version: config2.version ?? "unknown",
|
|
124
|
+
_env: config2.environment ?? "unknown"
|
|
125
|
+
};
|
|
126
|
+
if (config2.posthogApiKey) {
|
|
127
|
+
try {
|
|
128
|
+
const mod = await import('posthog-node');
|
|
129
|
+
const PostHog = mod.PostHog;
|
|
130
|
+
posthogClient = new PostHog(config2.posthogApiKey, {
|
|
131
|
+
host: "https://us.i.posthog.com",
|
|
132
|
+
flushAt: 10,
|
|
133
|
+
flushInterval: 1e4
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (config2.sentryDsn) {
|
|
139
|
+
try {
|
|
140
|
+
const mod = await import('@sentry/node');
|
|
141
|
+
sentryModule = mod;
|
|
142
|
+
sentryModule.init({
|
|
143
|
+
dsn: config2.sentryDsn,
|
|
144
|
+
environment: config2.environment ?? "cli",
|
|
145
|
+
release: config2.version,
|
|
146
|
+
tracesSampleRate: 0
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function trackNodeEvent(event, properties) {
|
|
153
|
+
if (!monitoringEnabled2 || !posthogClient) return;
|
|
154
|
+
try {
|
|
155
|
+
const captureOpts = {
|
|
156
|
+
distinctId: "anonymous",
|
|
157
|
+
event
|
|
158
|
+
};
|
|
159
|
+
captureOpts.properties = { ...commonProps, ...properties };
|
|
160
|
+
posthogClient.capture(captureOpts);
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function trackNodeError(error, context) {
|
|
165
|
+
if (!monitoringEnabled2) return;
|
|
166
|
+
try {
|
|
167
|
+
sentryModule?.captureException(error, context ? { extra: context } : void 0);
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
posthogClient?.capture({
|
|
172
|
+
distinctId: "anonymous",
|
|
173
|
+
event: "cic_error",
|
|
174
|
+
properties: { ...commonProps, error: error.message, ...context }
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function shutdownNodeMonitoring() {
|
|
180
|
+
if (!monitoringEnabled2) return;
|
|
181
|
+
const tasks = [];
|
|
182
|
+
if (posthogClient) {
|
|
183
|
+
tasks.push(
|
|
184
|
+
posthogClient.shutdown().catch(() => {
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (sentryModule) {
|
|
189
|
+
tasks.push(
|
|
190
|
+
sentryModule.close(2e3).catch(() => {
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
await Promise.allSettled(tasks);
|
|
195
|
+
posthogClient = null;
|
|
196
|
+
sentryModule = null;
|
|
197
|
+
monitoringEnabled2 = false;
|
|
198
|
+
}
|
|
199
|
+
var posthogClient, sentryModule, monitoringEnabled2, commonProps;
|
|
200
|
+
var init_node = __esm({
|
|
201
|
+
"src/monitoring/node.ts"() {
|
|
202
|
+
posthogClient = null;
|
|
203
|
+
sentryModule = null;
|
|
204
|
+
monitoringEnabled2 = false;
|
|
205
|
+
commonProps = {};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
10
208
|
z.object({
|
|
11
209
|
fileKey: z.string(),
|
|
12
210
|
nodeId: z.string().optional(),
|
|
@@ -185,7 +383,7 @@ var RULE_CONFIGS = {
|
|
|
185
383
|
score: -2,
|
|
186
384
|
enabled: true,
|
|
187
385
|
options: {
|
|
188
|
-
gridBase:
|
|
386
|
+
gridBase: 4
|
|
189
387
|
}
|
|
190
388
|
},
|
|
191
389
|
"magic-number-spacing": {
|
|
@@ -193,7 +391,7 @@ var RULE_CONFIGS = {
|
|
|
193
391
|
score: -4,
|
|
194
392
|
enabled: true,
|
|
195
393
|
options: {
|
|
196
|
-
gridBase:
|
|
394
|
+
gridBase: 4
|
|
197
395
|
}
|
|
198
396
|
},
|
|
199
397
|
"raw-shadow": {
|
|
@@ -764,6 +962,12 @@ function transformNode(node) {
|
|
|
764
962
|
if ("layoutPositioning" in node && node.layoutPositioning) {
|
|
765
963
|
base.layoutPositioning = node.layoutPositioning;
|
|
766
964
|
}
|
|
965
|
+
if ("layoutSizingHorizontal" in node && node.layoutSizingHorizontal) {
|
|
966
|
+
base.layoutSizingHorizontal = node.layoutSizingHorizontal;
|
|
967
|
+
}
|
|
968
|
+
if ("layoutSizingVertical" in node && node.layoutSizingVertical) {
|
|
969
|
+
base.layoutSizingVertical = node.layoutSizingVertical;
|
|
970
|
+
}
|
|
767
971
|
if ("primaryAxisAlignItems" in node) {
|
|
768
972
|
base.primaryAxisAlignItems = node.primaryAxisAlignItems;
|
|
769
973
|
}
|
|
@@ -1027,6 +1231,20 @@ function getReportsDir() {
|
|
|
1027
1231
|
function ensureReportsDir() {
|
|
1028
1232
|
ensureDir(REPORTS_DIR);
|
|
1029
1233
|
}
|
|
1234
|
+
function getTelemetryEnabled() {
|
|
1235
|
+
return readConfig().telemetry !== false;
|
|
1236
|
+
}
|
|
1237
|
+
function setTelemetryEnabled(enabled) {
|
|
1238
|
+
const config2 = readConfig();
|
|
1239
|
+
config2.telemetry = enabled;
|
|
1240
|
+
writeConfig(config2);
|
|
1241
|
+
}
|
|
1242
|
+
function getPosthogApiKey() {
|
|
1243
|
+
return process.env["POSTHOG_API_KEY"] ?? readConfig().posthogApiKey;
|
|
1244
|
+
}
|
|
1245
|
+
function getSentryDsn() {
|
|
1246
|
+
return process.env["SENTRY_DSN"] ?? readConfig().sentryDsn;
|
|
1247
|
+
}
|
|
1030
1248
|
function initAiready(token) {
|
|
1031
1249
|
setFigmaToken(token);
|
|
1032
1250
|
ensureReportsDir();
|
|
@@ -1552,7 +1770,7 @@ ${figmaToken ? ` <script>
|
|
|
1552
1770
|
const res = await fetch('https://api.figma.com/v1/files/' + fileKey + '/comments', {
|
|
1553
1771
|
method: 'POST',
|
|
1554
1772
|
headers: { 'X-FIGMA-TOKEN': FIGMA_TOKEN, 'Content-Type': 'application/json' },
|
|
1555
|
-
body: JSON.stringify({ message: commentBody, client_meta: { node_id: nodeId } }),
|
|
1773
|
+
body: JSON.stringify({ message: commentBody, client_meta: { node_id: nodeId, node_offset: { x: 0, y: 0 } } }),
|
|
1556
1774
|
});
|
|
1557
1775
|
if (!res.ok) throw new Error(await res.text());
|
|
1558
1776
|
btn.textContent = 'Sent \\u2713';
|
|
@@ -2315,6 +2533,12 @@ var ActivityLogger = class {
|
|
|
2315
2533
|
}
|
|
2316
2534
|
};
|
|
2317
2535
|
|
|
2536
|
+
// src/rules/excluded-names.ts
|
|
2537
|
+
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;
|
|
2538
|
+
function isExcludedName(name) {
|
|
2539
|
+
return EXCLUDED_NAME_PATTERN.test(name);
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2318
2542
|
// src/agents/orchestrator.ts
|
|
2319
2543
|
function selectNodes(summaries, strategy, maxNodes) {
|
|
2320
2544
|
if (summaries.length === 0) return [];
|
|
@@ -2370,14 +2594,13 @@ var ELIGIBLE_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2370
2594
|
"COMPONENT",
|
|
2371
2595
|
"INSTANCE"
|
|
2372
2596
|
]);
|
|
2373
|
-
var EXCLUDED_NAME_PATTERN = /\b(icon|ico|badge|indicator|image|asset|chatbot|cta|gnb|navigation|nav|fab|modal|dialog|popup|overlay|toast|snackbar|tooltip|dropdown|menu|sticky|bg|background|divider|separator|logo|avatar|thumbnail|thumb|header|footer|sidebar|toolbar|tabbar|tab-bar|statusbar|status-bar|spinner|loader|cursor|dot|dim|dimmed|filter)\b/i;
|
|
2374
2597
|
function filterConversionCandidates(summaries, documentRoot) {
|
|
2375
2598
|
return summaries.filter((summary) => {
|
|
2376
2599
|
const node = findNode(documentRoot, summary.nodeId);
|
|
2377
2600
|
if (!node) return false;
|
|
2378
2601
|
if (EXCLUDED_NODE_TYPES.has(node.type)) return false;
|
|
2379
2602
|
if (!ELIGIBLE_NODE_TYPES.has(node.type)) return false;
|
|
2380
|
-
if (
|
|
2603
|
+
if (isExcludedName(node.name)) return false;
|
|
2381
2604
|
const bbox = node.absoluteBoundingBox;
|
|
2382
2605
|
if (bbox && (bbox.width < MIN_WIDTH || bbox.height < MIN_HEIGHT)) return false;
|
|
2383
2606
|
if (!node.children || node.children.length < 3) return false;
|
|
@@ -2762,7 +2985,7 @@ Override canicode's default rule scores, severity, and filters.
|
|
|
2762
2985
|
STRUCTURE
|
|
2763
2986
|
- excludeNodeTypes: node types to skip (e.g. VECTOR, BOOLEAN_OPERATION)
|
|
2764
2987
|
- excludeNodeNames: name patterns to skip (e.g. icon, ico)
|
|
2765
|
-
- gridBase: spacing grid unit, default
|
|
2988
|
+
- gridBase: spacing grid unit, default 4
|
|
2766
2989
|
- colorTolerance: color diff tolerance, default 10
|
|
2767
2990
|
- rules: per-rule overrides (score, severity, enabled)
|
|
2768
2991
|
|
|
@@ -2807,6 +3030,77 @@ function handleDocs(topic) {
|
|
|
2807
3030
|
}
|
|
2808
3031
|
}
|
|
2809
3032
|
|
|
3033
|
+
// src/monitoring/events.ts
|
|
3034
|
+
var EVENT_PREFIX = "cic_";
|
|
3035
|
+
var EVENTS = {
|
|
3036
|
+
// Analysis
|
|
3037
|
+
ANALYSIS_STARTED: `${EVENT_PREFIX}analysis_started`,
|
|
3038
|
+
ANALYSIS_COMPLETED: `${EVENT_PREFIX}analysis_completed`,
|
|
3039
|
+
ANALYSIS_FAILED: `${EVENT_PREFIX}analysis_failed`,
|
|
3040
|
+
// Report
|
|
3041
|
+
REPORT_GENERATED: `${EVENT_PREFIX}report_generated`,
|
|
3042
|
+
COMMENT_POSTED: `${EVENT_PREFIX}comment_posted`,
|
|
3043
|
+
COMMENT_FAILED: `${EVENT_PREFIX}comment_failed`,
|
|
3044
|
+
// MCP
|
|
3045
|
+
MCP_TOOL_CALLED: `${EVENT_PREFIX}mcp_tool_called`,
|
|
3046
|
+
// CLI
|
|
3047
|
+
CLI_COMMAND: `${EVENT_PREFIX}cli_command`,
|
|
3048
|
+
CLI_INIT: `${EVENT_PREFIX}cli_init`
|
|
3049
|
+
};
|
|
3050
|
+
|
|
3051
|
+
// src/monitoring/index.ts
|
|
3052
|
+
var _trackEvent = () => {
|
|
3053
|
+
};
|
|
3054
|
+
var _trackError = () => {
|
|
3055
|
+
};
|
|
3056
|
+
var _shutdown = () => Promise.resolve();
|
|
3057
|
+
function isBrowser() {
|
|
3058
|
+
const g = globalThis;
|
|
3059
|
+
return typeof g["window"] !== "undefined" && typeof g["document"] !== "undefined";
|
|
3060
|
+
}
|
|
3061
|
+
async function initMonitoring(config2) {
|
|
3062
|
+
if (config2.enabled === false) return;
|
|
3063
|
+
if (!config2.posthogApiKey && !config2.sentryDsn) return;
|
|
3064
|
+
try {
|
|
3065
|
+
if (isBrowser()) {
|
|
3066
|
+
const { initBrowserMonitoring: initBrowserMonitoring2, trackBrowserEvent: trackBrowserEvent2, trackBrowserError: trackBrowserError2, shutdownBrowserMonitoring: shutdownBrowserMonitoring2 } = await Promise.resolve().then(() => (init_browser(), browser_exports));
|
|
3067
|
+
await initBrowserMonitoring2(config2);
|
|
3068
|
+
_trackEvent = trackBrowserEvent2;
|
|
3069
|
+
_trackError = trackBrowserError2;
|
|
3070
|
+
_shutdown = shutdownBrowserMonitoring2;
|
|
3071
|
+
} else {
|
|
3072
|
+
const { initNodeMonitoring: initNodeMonitoring2, trackNodeEvent: trackNodeEvent2, trackNodeError: trackNodeError2, shutdownNodeMonitoring: shutdownNodeMonitoring2 } = await Promise.resolve().then(() => (init_node(), node_exports));
|
|
3073
|
+
await initNodeMonitoring2(config2);
|
|
3074
|
+
_trackEvent = trackNodeEvent2;
|
|
3075
|
+
_trackError = trackNodeError2;
|
|
3076
|
+
_shutdown = shutdownNodeMonitoring2;
|
|
3077
|
+
}
|
|
3078
|
+
} catch {
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
function trackEvent(event, properties) {
|
|
3082
|
+
try {
|
|
3083
|
+
_trackEvent(event, properties);
|
|
3084
|
+
} catch {
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
function trackError(error, context) {
|
|
3088
|
+
try {
|
|
3089
|
+
_trackError(error, context);
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
async function shutdownMonitoring() {
|
|
3094
|
+
try {
|
|
3095
|
+
await _shutdown();
|
|
3096
|
+
} catch {
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
// src/monitoring/keys.ts
|
|
3101
|
+
var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
|
|
3102
|
+
var SENTRY_DSN = "https://80a836a8300b25f17ef5bbf23afb5b3a@o4511080656207872.ingest.us.sentry.io/4511080661319680" ;
|
|
3103
|
+
|
|
2810
3104
|
// src/rules/layout/index.ts
|
|
2811
3105
|
function isContainerNode(node) {
|
|
2812
3106
|
return node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT";
|
|
@@ -2848,7 +3142,6 @@ var absolutePositionInAutoLayoutDef = {
|
|
|
2848
3142
|
impact: "Element will not respond to sibling changes, may overlap unexpectedly",
|
|
2849
3143
|
fix: "Remove absolute positioning or use proper Auto Layout alignment"
|
|
2850
3144
|
};
|
|
2851
|
-
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;
|
|
2852
3145
|
function isSmallRelativeToParent(node, parent) {
|
|
2853
3146
|
const nodeBB = node.absoluteBoundingBox;
|
|
2854
3147
|
const parentBB = parent.absoluteBoundingBox;
|
|
@@ -2862,7 +3155,8 @@ var absolutePositionInAutoLayoutCheck = (node, context) => {
|
|
|
2862
3155
|
if (!context.parent) return null;
|
|
2863
3156
|
if (!hasAutoLayout(context.parent)) return null;
|
|
2864
3157
|
if (node.layoutPositioning !== "ABSOLUTE") return null;
|
|
2865
|
-
if (
|
|
3158
|
+
if (node.type === "VECTOR" || node.type === "BOOLEAN_OPERATION" || node.type === "LINE" || node.type === "ELLIPSE" || node.type === "STAR" || node.type === "REGULAR_POLYGON") return null;
|
|
3159
|
+
if (isExcludedName(node.name)) return null;
|
|
2866
3160
|
if (isSmallRelativeToParent(node, context.parent)) return null;
|
|
2867
3161
|
if (context.parent.type === "COMPONENT") return null;
|
|
2868
3162
|
return {
|
|
@@ -2888,10 +3182,14 @@ var fixedWidthInResponsiveContextCheck = (node, context) => {
|
|
|
2888
3182
|
if (!context.parent) return null;
|
|
2889
3183
|
if (!hasAutoLayout(context.parent)) return null;
|
|
2890
3184
|
if (!isContainerNode(node)) return null;
|
|
2891
|
-
if (node.
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3185
|
+
if (node.layoutSizingHorizontal) {
|
|
3186
|
+
if (node.layoutSizingHorizontal !== "FIXED") return null;
|
|
3187
|
+
} else {
|
|
3188
|
+
if (node.layoutAlign === "STRETCH") return null;
|
|
3189
|
+
if (!node.absoluteBoundingBox) return null;
|
|
3190
|
+
if (node.layoutAlign !== "INHERIT") return null;
|
|
3191
|
+
}
|
|
3192
|
+
if (isExcludedName(node.name)) return null;
|
|
2895
3193
|
return {
|
|
2896
3194
|
ruleId: fixedWidthInResponsiveContextDef.id,
|
|
2897
3195
|
nodeId: node.id,
|
|
@@ -3164,7 +3462,7 @@ var inconsistentSpacingDef = {
|
|
|
3164
3462
|
fix: "Use spacing values from the design system grid (e.g., 8pt increments)"
|
|
3165
3463
|
};
|
|
3166
3464
|
var inconsistentSpacingCheck = (node, context, options) => {
|
|
3167
|
-
const gridBase = options?.["gridBase"] ?? getRuleOption("inconsistent-spacing", "gridBase",
|
|
3465
|
+
const gridBase = options?.["gridBase"] ?? getRuleOption("inconsistent-spacing", "gridBase", 4);
|
|
3168
3466
|
const paddings = [
|
|
3169
3467
|
node.paddingLeft,
|
|
3170
3468
|
node.paddingRight,
|
|
@@ -3206,7 +3504,7 @@ var magicNumberSpacingDef = {
|
|
|
3206
3504
|
fix: "Round spacing to the nearest grid value or use spacing tokens"
|
|
3207
3505
|
};
|
|
3208
3506
|
var magicNumberSpacingCheck = (node, context, options) => {
|
|
3209
|
-
const gridBase = options?.["gridBase"] ?? getRuleOption("magic-number-spacing", "gridBase",
|
|
3507
|
+
const gridBase = options?.["gridBase"] ?? getRuleOption("magic-number-spacing", "gridBase", 4);
|
|
3210
3508
|
const allSpacings = [
|
|
3211
3509
|
node.paddingLeft,
|
|
3212
3510
|
node.paddingRight,
|
|
@@ -3523,6 +3821,7 @@ var defaultNameDef = {
|
|
|
3523
3821
|
};
|
|
3524
3822
|
var defaultNameCheck = (node, context) => {
|
|
3525
3823
|
if (!node.name) return null;
|
|
3824
|
+
if (isExcludedName(node.name)) return null;
|
|
3526
3825
|
if (!isDefaultName(node.name)) return null;
|
|
3527
3826
|
return {
|
|
3528
3827
|
ruleId: defaultNameDef.id,
|
|
@@ -3545,6 +3844,7 @@ var nonSemanticNameDef = {
|
|
|
3545
3844
|
};
|
|
3546
3845
|
var nonSemanticNameCheck = (node, context) => {
|
|
3547
3846
|
if (!node.name) return null;
|
|
3847
|
+
if (isExcludedName(node.name)) return null;
|
|
3548
3848
|
if (!isNonSemanticName(node.name)) return null;
|
|
3549
3849
|
if (!node.children || node.children.length === 0) {
|
|
3550
3850
|
const shapeTypes = ["RECTANGLE", "ELLIPSE", "VECTOR", "LINE", "STAR", "REGULAR_POLYGON"];
|
|
@@ -3613,6 +3913,7 @@ var numericSuffixNameDef = {
|
|
|
3613
3913
|
};
|
|
3614
3914
|
var numericSuffixNameCheck = (node, context) => {
|
|
3615
3915
|
if (!node.name) return null;
|
|
3916
|
+
if (isExcludedName(node.name)) return null;
|
|
3616
3917
|
if (isDefaultName(node.name)) return null;
|
|
3617
3918
|
if (!hasNumericSuffix(node.name)) return null;
|
|
3618
3919
|
return {
|
|
@@ -3969,6 +4270,23 @@ defineRule({
|
|
|
3969
4270
|
// src/cli/index.ts
|
|
3970
4271
|
config();
|
|
3971
4272
|
var cli = cac("canicode");
|
|
4273
|
+
{
|
|
4274
|
+
const monitoringConfig = {
|
|
4275
|
+
environment: "cli",
|
|
4276
|
+
version: "0.3.3",
|
|
4277
|
+
enabled: getTelemetryEnabled()
|
|
4278
|
+
};
|
|
4279
|
+
const phKey = getPosthogApiKey() || POSTHOG_API_KEY;
|
|
4280
|
+
monitoringConfig.posthogApiKey = phKey;
|
|
4281
|
+
const sDsn = getSentryDsn() || SENTRY_DSN;
|
|
4282
|
+
monitoringConfig.sentryDsn = sDsn;
|
|
4283
|
+
initMonitoring(monitoringConfig).catch(() => {
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4286
|
+
process.on("beforeExit", () => {
|
|
4287
|
+
shutdownMonitoring().catch(() => {
|
|
4288
|
+
});
|
|
4289
|
+
});
|
|
3972
4290
|
var MAX_NODES_WITHOUT_SCOPE = 500;
|
|
3973
4291
|
function pickRandomScope(root) {
|
|
3974
4292
|
const candidates = [];
|
|
@@ -4001,6 +4319,8 @@ function countNodes2(node) {
|
|
|
4001
4319
|
return count;
|
|
4002
4320
|
}
|
|
4003
4321
|
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("--mcp", "Load via Figma MCP (no FIGMA_TOKEN needed)").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 --mcp").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) => {
|
|
4322
|
+
const analysisStart = Date.now();
|
|
4323
|
+
trackEvent(EVENTS.ANALYSIS_STARTED, { source: isJsonFile(input) ? "fixture" : "figma" });
|
|
4004
4324
|
try {
|
|
4005
4325
|
if (options.mcp && options.api) {
|
|
4006
4326
|
throw new Error("Cannot use --mcp and --api together. Choose one.");
|
|
@@ -4099,6 +4419,14 @@ Analyzing: ${file.name}`);
|
|
|
4099
4419
|
await writeFile(outputPath, html, "utf-8");
|
|
4100
4420
|
console.log(`
|
|
4101
4421
|
Report saved: ${outputPath}`);
|
|
4422
|
+
trackEvent(EVENTS.ANALYSIS_COMPLETED, {
|
|
4423
|
+
nodeCount: result.nodeCount,
|
|
4424
|
+
issueCount: result.issues.length,
|
|
4425
|
+
grade: scores.overall.grade,
|
|
4426
|
+
percentage: scores.overall.percentage,
|
|
4427
|
+
duration: Date.now() - analysisStart
|
|
4428
|
+
});
|
|
4429
|
+
trackEvent(EVENTS.REPORT_GENERATED, { format: "html" });
|
|
4102
4430
|
if (!options.noOpen) {
|
|
4103
4431
|
const { exec } = await import('child_process');
|
|
4104
4432
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
@@ -4108,6 +4436,14 @@ Report saved: ${outputPath}`);
|
|
|
4108
4436
|
process.exit(1);
|
|
4109
4437
|
}
|
|
4110
4438
|
} catch (error) {
|
|
4439
|
+
trackError(
|
|
4440
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
4441
|
+
{ command: "analyze", input }
|
|
4442
|
+
);
|
|
4443
|
+
trackEvent(EVENTS.ANALYSIS_FAILED, {
|
|
4444
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4445
|
+
duration: Date.now() - analysisStart
|
|
4446
|
+
});
|
|
4111
4447
|
console.error(
|
|
4112
4448
|
"\nError:",
|
|
4113
4449
|
error instanceof Error ? error.message : String(error)
|
|
@@ -4364,6 +4700,35 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
|
|
|
4364
4700
|
process.exit(1);
|
|
4365
4701
|
}
|
|
4366
4702
|
});
|
|
4703
|
+
cli.command("config", "Manage canicode configuration").option("--telemetry", "Enable anonymous telemetry").option("--no-telemetry", "Disable anonymous telemetry").action((options) => {
|
|
4704
|
+
try {
|
|
4705
|
+
if (options.noTelemetry === true) {
|
|
4706
|
+
setTelemetryEnabled(false);
|
|
4707
|
+
console.log("Telemetry disabled. No analytics data will be sent.");
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
if (options.telemetry === true) {
|
|
4711
|
+
setTelemetryEnabled(true);
|
|
4712
|
+
console.log("Telemetry enabled. Only anonymous usage events are tracked \u2014 no design data.");
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
const cfg = readConfig();
|
|
4716
|
+
console.log("CANICODE CONFIG\n");
|
|
4717
|
+
console.log(` Config path: ${getConfigPath()}`);
|
|
4718
|
+
console.log(` Figma token: ${cfg.figmaToken ? "set" : "not set"}`);
|
|
4719
|
+
console.log(` Telemetry: ${cfg.telemetry !== false ? "enabled" : "disabled"}`);
|
|
4720
|
+
console.log(`
|
|
4721
|
+
Options:`);
|
|
4722
|
+
console.log(` canicode config --no-telemetry Opt out of anonymous telemetry`);
|
|
4723
|
+
console.log(` canicode config --telemetry Opt back in`);
|
|
4724
|
+
} catch (error) {
|
|
4725
|
+
console.error(
|
|
4726
|
+
"\nError:",
|
|
4727
|
+
error instanceof Error ? error.message : String(error)
|
|
4728
|
+
);
|
|
4729
|
+
process.exit(1);
|
|
4730
|
+
}
|
|
4731
|
+
});
|
|
4367
4732
|
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) => {
|
|
4368
4733
|
try {
|
|
4369
4734
|
let configs = { ...RULE_CONFIGS };
|