canicode 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,33 +144,6 @@ canicode analyze <url> --preset strict
144
144
 
145
145
  </details>
146
146
 
147
- <details>
148
- <summary><strong>Custom Rules</strong></summary>
149
-
150
- Add project-specific checks via a JSON file:
151
-
152
- ```bash
153
- canicode analyze <url> --custom-rules ./my-rules.json
154
- ```
155
-
156
- ```json
157
- [
158
- {
159
- "id": "icon-missing-component",
160
- "category": "component",
161
- "severity": "blocking",
162
- "score": -10,
163
- "prompt": "Check if this node is an icon and is not a component.",
164
- "why": "Icons that are not components cannot be reused.",
165
- "impact": "Developers will hardcode icons.",
166
- "fix": "Convert to a component and publish to the library."
167
- }
168
- ]
169
- ```
170
-
171
- See [`examples/custom-rules.json`](examples/custom-rules.json) | Run `canicode docs rules`
172
-
173
- </details>
174
147
 
175
148
  <details>
176
149
  <summary><strong>Config Overrides</strong></summary>
@@ -308,9 +281,10 @@ pnpm lint # type check
308
281
 
309
282
  - [x] **Phase 1** — 39 rules, density-based scoring, HTML reports, presets, scoped analysis
310
283
  - [x] **Phase 2** — 4-agent calibration pipeline, `/calibrate-loop` debate loop
311
- - [x] **Phase 3** — Custom rules, config overrides, MCP server, Claude Skills
284
+ - [x] **Phase 3** — Config overrides, MCP server, Claude Skills
312
285
  - [x] **Phase 4** — Figma comment from report (per-issue "Comment" button in HTML report, posts to Figma node via API)
313
- - [ ] **Phase 5** — Screenshot comparison (Figma vs AI-generated code, visual diff)
286
+ - [ ] **Phase 5** — Custom rules with pattern matching (node name/type/attribute conditions)
287
+ - [ ] **Phase 6** — Screenshot comparison (Figma vs AI-generated code, visual diff)
314
288
 
315
289
  ## License
316
290
 
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(),
@@ -1033,6 +1231,20 @@ function getReportsDir() {
1033
1231
  function ensureReportsDir() {
1034
1232
  ensureDir(REPORTS_DIR);
1035
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
+ }
1036
1248
  function initAiready(token) {
1037
1249
  setFigmaToken(token);
1038
1250
  ensureReportsDir();
@@ -2818,6 +3030,77 @@ function handleDocs(topic) {
2818
3030
  }
2819
3031
  }
2820
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
+
2821
3104
  // src/rules/layout/index.ts
2822
3105
  function isContainerNode(node) {
2823
3106
  return node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT";
@@ -3987,6 +4270,23 @@ defineRule({
3987
4270
  // src/cli/index.ts
3988
4271
  config();
3989
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
+ });
3990
4290
  var MAX_NODES_WITHOUT_SCOPE = 500;
3991
4291
  function pickRandomScope(root) {
3992
4292
  const candidates = [];
@@ -4019,6 +4319,8 @@ function countNodes2(node) {
4019
4319
  return count;
4020
4320
  }
4021
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" });
4022
4324
  try {
4023
4325
  if (options.mcp && options.api) {
4024
4326
  throw new Error("Cannot use --mcp and --api together. Choose one.");
@@ -4117,6 +4419,14 @@ Analyzing: ${file.name}`);
4117
4419
  await writeFile(outputPath, html, "utf-8");
4118
4420
  console.log(`
4119
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" });
4120
4430
  if (!options.noOpen) {
4121
4431
  const { exec } = await import('child_process');
4122
4432
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
@@ -4126,6 +4436,14 @@ Report saved: ${outputPath}`);
4126
4436
  process.exit(1);
4127
4437
  }
4128
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
+ });
4129
4447
  console.error(
4130
4448
  "\nError:",
4131
4449
  error instanceof Error ? error.message : String(error)
@@ -4382,6 +4700,35 @@ cli.command("init", "Set up canicode (Figma token or MCP)").option("--token <tok
4382
4700
  process.exit(1);
4383
4701
  }
4384
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
+ });
4385
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) => {
4386
4733
  try {
4387
4734
  let configs = { ...RULE_CONFIGS };
@@ -4460,7 +4807,6 @@ cli.help((sections) => {
4460
4807
  {
4461
4808
  title: "\nCustomization",
4462
4809
  body: [
4463
- ` --custom-rules <path> Add custom rules (see: canicode docs rules)`,
4464
4810
  ` --config <path> Override rule settings (see: canicode docs config)`
4465
4811
  ].join("\n")
4466
4812
  },
@@ -4470,8 +4816,7 @@ cli.help((sections) => {
4470
4816
  ` $ canicode analyze "https://www.figma.com/design/..." --mcp`,
4471
4817
  ` $ canicode analyze "https://www.figma.com/design/..." --api`,
4472
4818
  ` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
4473
- ` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`,
4474
- ` $ canicode analyze "https://www.figma.com/design/..." --custom-rules ./my-rules.json`
4819
+ ` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`
4475
4820
  ].join("\n")
4476
4821
  },
4477
4822
  {