canicode 0.3.3 → 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/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 };