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 +347 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +308 -0
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +31 -0
- package/package.json +1 -1
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 };
|