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 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: 8) |
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: 8
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: 8
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 (EXCLUDED_NAME_PATTERN.test(node.name)) return false;
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 8
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 (INTENTIONAL_ABSOLUTE_PATTERNS.test(node.name)) return null;
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.layoutAlign === "STRETCH") return null;
2892
- const bbox = node.absoluteBoundingBox;
2893
- if (!bbox) return null;
2894
- if (node.layoutAlign !== "INHERIT") return null;
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", 8);
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", 8);
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 };