canicode 0.11.5 → 0.12.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/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync, renameSync, chmodSync, copyFileSync } from 'fs';
3
- import { join, resolve, dirname, basename, relative } from 'path';
3
+ import { join, resolve, dirname, basename, isAbsolute, sep, relative } from 'path';
4
4
  import pixelmatch from 'pixelmatch';
5
5
  import { PNG } from 'pngjs';
6
6
  import { createRequire } from 'module';
@@ -10,6 +10,7 @@ import { randomUUID } from 'crypto';
10
10
  import { homedir } from 'os';
11
11
  import { z } from 'zod';
12
12
  import { readFile, writeFile } from 'fs/promises';
13
+ import * as readline from 'readline/promises';
13
14
  import { createInterface } from 'readline/promises';
14
15
  import { pathToFileURL, fileURLToPath } from 'url';
15
16
 
@@ -131,6 +132,32 @@ var init_figma_client = __esm({
131
132
  const buffer = await response.arrayBuffer();
132
133
  return Buffer.from(buffer).toString("base64");
133
134
  }
135
+ /**
136
+ * Get the components a file has published to a team library.
137
+ *
138
+ * `GET /v1/files/:file_key/components` returns only components that have
139
+ * been pushed via the Publish Library action — local-but-unpublished
140
+ * components are absent. This is the authoritative way to detect whether
141
+ * a Figma component is mappable via Code Connect (#532): `add_code_connect_map`
142
+ * requires a published component and otherwise fails with "Published
143
+ * component not found."
144
+ */
145
+ async getPublishedComponents(fileKey) {
146
+ const url = `${FIGMA_API_BASE}/files/${fileKey}/components`;
147
+ const response = await fetch(url, {
148
+ headers: { "X-Figma-Token": this.token }
149
+ });
150
+ if (!response.ok) {
151
+ const error = await response.json().catch(() => ({}));
152
+ throw new FigmaClientError(
153
+ `Failed to fetch published components: ${response.status} ${response.statusText}`,
154
+ response.status,
155
+ error
156
+ );
157
+ }
158
+ const data = await response.json();
159
+ return data.meta?.components ?? [];
160
+ }
134
161
  async getFileNodes(fileKey, nodeIds) {
135
162
  const ids = nodeIds.join(",");
136
163
  const url = `${FIGMA_API_BASE}/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}`;
@@ -1371,6 +1398,8 @@ var EVENTS = {
1371
1398
  // CLI
1372
1399
  CLI_COMMAND: `${EVENT_PREFIX}cli_command`,
1373
1400
  CLI_INIT: `${EVENT_PREFIX}cli_init`,
1401
+ CLI_CONFIG_SET_TOKEN: `${EVENT_PREFIX}cli_config_set_token`,
1402
+ CLI_DOCTOR: `${EVENT_PREFIX}cli_doctor`,
1374
1403
  // Roundtrip (ADR-012)
1375
1404
  // Wiring point for the roundtrip helper's `telemetry` callback. No Node-side
1376
1405
  // orchestrator reads this yet — the helper ships in a sandbox-pure IIFE that
@@ -1547,12 +1576,14 @@ CANICODE SETUP GUIDE
1547
1576
  npm install -g canicode
1548
1577
 
1549
1578
  Setup:
1550
- canicode init --token figd_xxxxxxxxxxxxx
1579
+ canicode init (interactive prompt; TTY)
1580
+ canicode init --token figd_xxxxxxxxxxxxx (non-TTY / CI)
1581
+ FIGMA_TOKEN=figd_xxx canicode init (env-driven)
1551
1582
  (saves token + installs Claude Code skills into ./.claude/skills/)
1552
1583
 
1553
1584
  Skills only (no token yet):
1554
1585
  canicode init --cursor-skills
1555
- (installs Claude skills + Cursor copies; run init --token \u2026 before live Figma REST URLs)
1586
+ (installs Claude skills + Cursor copies; run init or init --token \u2026 before live Figma REST URLs)
1556
1587
 
1557
1588
  Use:
1558
1589
  canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
@@ -1562,7 +1593,7 @@ CANICODE SETUP GUIDE
1562
1593
  --preset strict|relaxed|dev-friendly|ai-ready
1563
1594
  --config ./my-config.json
1564
1595
  --no-open Don't open report in browser
1565
- --api No-op for Figma URLs (REST always); same flag as gotcha-survey (#461)
1596
+ --api No-op for Figma URLs (REST always); same flag as gotcha-survey
1566
1597
 
1567
1598
  Output:
1568
1599
  ~/.canicode/reports/report-YYYY-MM-DD-HH-mm-<filekey>.html
@@ -1574,7 +1605,8 @@ CANICODE SETUP GUIDE
1574
1605
  (Same token safety as above \u2014 env var or interactive prompt, not chat.)
1575
1606
 
1576
1607
  Setup:
1577
- canicode init --token figd_xxxxxxxxxxxxx
1608
+ canicode init (interactive prompt; TTY)
1609
+ canicode init --token figd_xxxxxxxxxxxxx (non-TTY / CI)
1578
1610
  (installs three skills into ./.claude/skills/ alongside the token)
1579
1611
 
1580
1612
  Installed skills:
@@ -1599,7 +1631,8 @@ CANICODE SETUP GUIDE
1599
1631
  (Same token safety as above \u2014 env var or interactive prompt, not chat.)
1600
1632
 
1601
1633
  Setup:
1602
- canicode init --token figd_xxxxxxxxxxxxx --cursor-skills
1634
+ canicode init --cursor-skills (interactive prompt; TTY)
1635
+ canicode init --token figd_xxxxxxxxxxxxx --cursor-skills (non-TTY / CI)
1603
1636
  (installs Cursor copies of the three skills into ./.cursor/skills/)
1604
1637
 
1605
1638
  Installed skills:
@@ -1629,6 +1662,14 @@ CANICODE SETUP GUIDE
1629
1662
  See also: docs/CUSTOMIZATION.md#cursor-mcp-canicode (Figma MCP required for roundtrip
1630
1663
  writes; analyze-only works without it).
1631
1664
 
1665
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1666
+ MANAGE CONFIG
1667
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1668
+
1669
+ canicode config set-token Rotate Figma token (no skill reinstall)
1670
+ canicode config show Print masked token + config + reports paths
1671
+ canicode config path Print absolute config path (script-friendly)
1672
+
1632
1673
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1633
1674
  TOKEN PRIORITY
1634
1675
  \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
@@ -1821,6 +1862,7 @@ var RULE_ID_CATEGORY = {
1821
1862
  "detached-instance": "code-quality",
1822
1863
  "variant-structure-mismatch": "code-quality",
1823
1864
  "deep-nesting": "code-quality",
1865
+ "unmapped-component": "code-quality",
1824
1866
  // Token Management
1825
1867
  "raw-value": "token-management",
1826
1868
  "irregular-spacing": "token-management",
@@ -1851,6 +1893,12 @@ var RULE_PURPOSE = {
1851
1893
  "detached-instance": "violation",
1852
1894
  "variant-structure-mismatch": "violation",
1853
1895
  "deep-nesting": "violation",
1896
+ // #520: unmapped-component is annotation-primary. Fires only when the
1897
+ // user has Code Connect set up at all (figma.config.json present in cwd).
1898
+ // The gotcha drives the user to /canicode-roundtrip for actual mapping
1899
+ // registration via the Figma MCP tools — analyze itself does not parse
1900
+ // mapping declarations (deferred to v1.5).
1901
+ "unmapped-component": "info-collection",
1854
1902
  // Token Management
1855
1903
  "raw-value": "violation",
1856
1904
  "irregular-spacing": "violation",
@@ -1894,12 +1942,12 @@ var RULE_CONFIGS = {
1894
1942
  enabled: true
1895
1943
  },
1896
1944
  "missing-size-constraint": {
1897
- // #403: severity downgraded `risk missing-info` and score from
1898
- // -8 -1 to match the new info-collection purpose. Keeping the
1899
- // rule enabled (not disabled) so its gotchas still surface in the
1900
- // survey see RULE_PURPOSE entry above for the full rationale.
1901
- severity: "missing-info",
1902
- score: -1,
1945
+ // #403 → #519: info-collection rule. Score is 0 (severity `note`):
1946
+ // its value is the gotcha annotation, not the grade impact. Survey-
1947
+ // generator includes this rule via the `purpose === "info-collection"`
1948
+ // branch so the gotcha keeps surfacing.
1949
+ severity: "note",
1950
+ score: 0,
1903
1951
  enabled: true
1904
1952
  },
1905
1953
  // ── Code Quality ──
@@ -1931,6 +1979,16 @@ var RULE_CONFIGS = {
1931
1979
  maxDepth: 5
1932
1980
  }
1933
1981
  },
1982
+ "unmapped-component": {
1983
+ // #520 / #519: zero-impact tier. Fires per main component when Code
1984
+ // Connect is set up in the consuming repo (figma.config.json at cwd).
1985
+ // Score is 0 because the rule's value is the gotcha + roundtrip handoff,
1986
+ // not the grade signal — designers who deliberately do not map (e.g.
1987
+ // marketing-only banners) are not punished.
1988
+ severity: "note",
1989
+ score: 0,
1990
+ enabled: true
1991
+ },
1934
1992
  // ── Token Management ──
1935
1993
  "raw-value": {
1936
1994
  severity: "missing-info",
@@ -1952,15 +2010,15 @@ var RULE_CONFIGS = {
1952
2010
  // is minimal. Score stays at -1 so re-enabling `missing-prototype` on
1953
2011
  // fixtures that lack `interactionDestinations` (#139) cannot swing grades.
1954
2012
  "missing-interaction-state": {
1955
- severity: "missing-info",
1956
- score: -1,
1957
- // uncalibrated: no metric to validate score (#210), kept at -1 to preserve category visibility
2013
+ severity: "note",
2014
+ // #519: info-collection rule, zero-score tier
2015
+ score: 0,
1958
2016
  enabled: true
1959
2017
  },
1960
2018
  "missing-prototype": {
1961
- severity: "missing-info",
1962
- score: -1,
1963
- // #406: info-collection — annotation is primary output; score kept minimal so #139 fixtures don't skew calibration
2019
+ severity: "note",
2020
+ // #519: info-collection — annotation is primary output, no grade impact
2021
+ score: 0,
1964
2022
  enabled: true
1965
2023
  },
1966
2024
  // ── Semantic ──
@@ -2165,7 +2223,14 @@ var SeveritySchema = z.enum([
2165
2223
  "blocking",
2166
2224
  "risk",
2167
2225
  "missing-info",
2168
- "suggestion"
2226
+ "suggestion",
2227
+ /**
2228
+ * `note` is the zero-impact tier (#519): findings render in the report but
2229
+ * never move the grade. Used for annotation-primary rules whose value is the
2230
+ * nudge, not the score (e.g. unmapped Code Connect components, info-collection
2231
+ * rules whose answers belong in figma-implement-design context, not in linting).
2232
+ */
2233
+ "note"
2169
2234
  ]);
2170
2235
 
2171
2236
  // src/core/contracts/rule.ts
@@ -2459,6 +2524,10 @@ var missingComponentMsg = {
2459
2524
  suggestion: `Create a new variant for this style combination`
2460
2525
  })
2461
2526
  };
2527
+ var unmappedComponentMsg = (componentName) => ({
2528
+ message: `"${componentName}" has no Code Connect mapping`,
2529
+ suggestion: `Run /canicode-roundtrip on this component to register a mapping so figma-implement-design reuses your code instead of regenerating markup. Skip if intentionally unmapped.`
2530
+ });
2462
2531
  var detachedInstanceMsg = (name, componentName) => ({
2463
2532
  message: `"${name}" may be a detached instance of component "${componentName}"`,
2464
2533
  suggestion: `Restore as an instance of "${componentName}" or create a new variant`
@@ -3010,6 +3079,174 @@ defineRule({
3010
3079
  definition: irregularSpacingDef,
3011
3080
  check: irregularSpacingCheck
3012
3081
  });
3082
+ var FIGMA_CONFIG_FILENAME = "figma.config.json";
3083
+ var FIGMA_CONNECT_FILE_GLOB = /\.figma\.(tsx?|jsx?)$/;
3084
+ var NODE_ID_QUERY_RE = /[?&]node-id=([0-9A-Za-z%:\-_]+)/;
3085
+ function parseCodeConnectMappings(cwd) {
3086
+ const configPath = join(cwd, FIGMA_CONFIG_FILENAME);
3087
+ if (!existsSync(configPath)) {
3088
+ return {
3089
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3090
+ scannedFiles: [],
3091
+ skipReason: "no-config",
3092
+ skippedReason: `${FIGMA_CONFIG_FILENAME} not found at ${cwd}`
3093
+ };
3094
+ }
3095
+ let config2;
3096
+ try {
3097
+ config2 = JSON.parse(readFileSync(configPath, "utf-8"));
3098
+ } catch (err) {
3099
+ return {
3100
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3101
+ scannedFiles: [],
3102
+ skipReason: "malformed-config",
3103
+ skippedReason: `malformed ${FIGMA_CONFIG_FILENAME}: ${err.message}`
3104
+ };
3105
+ }
3106
+ const includes = config2.codeConnect?.include ?? config2.include ?? [];
3107
+ if (includes.length === 0) {
3108
+ return {
3109
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3110
+ scannedFiles: [],
3111
+ skipReason: "no-includes",
3112
+ skippedReason: `${FIGMA_CONFIG_FILENAME} has no codeConnect.include paths`
3113
+ };
3114
+ }
3115
+ const candidateFiles = /* @__PURE__ */ new Set();
3116
+ for (const includePattern of includes) {
3117
+ for (const file of resolveInclude(cwd, includePattern)) {
3118
+ candidateFiles.add(file);
3119
+ }
3120
+ }
3121
+ const mappedNodeIds = /* @__PURE__ */ new Set();
3122
+ const scannedFiles = [];
3123
+ for (const file of candidateFiles) {
3124
+ scannedFiles.push(file);
3125
+ let contents;
3126
+ try {
3127
+ contents = readFileSync(file, "utf-8");
3128
+ } catch {
3129
+ continue;
3130
+ }
3131
+ for (const nodeId of extractNodeIdsFromSource(contents)) {
3132
+ mappedNodeIds.add(nodeId);
3133
+ }
3134
+ }
3135
+ return { mappedNodeIds, scannedFiles };
3136
+ }
3137
+ function resolveInclude(cwd, includePattern) {
3138
+ const results = [];
3139
+ const absolute = isAbsolute(includePattern) ? includePattern : resolve(cwd, includePattern);
3140
+ const segments = absolute.split(sep);
3141
+ let firstGlobIdx = segments.findIndex((s) => /[*?{[]/.test(s));
3142
+ if (firstGlobIdx === -1) {
3143
+ if (existsSync(absolute)) {
3144
+ const stat = statSync(absolute);
3145
+ if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(absolute)) {
3146
+ results.push(absolute);
3147
+ } else if (stat.isDirectory()) {
3148
+ walkDir(absolute, results);
3149
+ }
3150
+ }
3151
+ return results;
3152
+ }
3153
+ const rootSegments = segments.slice(0, firstGlobIdx);
3154
+ const root = rootSegments.length === 0 ? sep : rootSegments.join(sep);
3155
+ if (!existsSync(root)) return results;
3156
+ const rootStat = statSync(root);
3157
+ if (!rootStat.isDirectory()) return results;
3158
+ walkDir(root, results);
3159
+ const prefix = rootSegments.join(sep) + sep;
3160
+ return results.filter((f) => f.startsWith(prefix) || rootSegments.length === 0);
3161
+ }
3162
+ function walkDir(dir, out) {
3163
+ let entries;
3164
+ try {
3165
+ entries = readdirSync(dir);
3166
+ } catch {
3167
+ return;
3168
+ }
3169
+ for (const entry of entries) {
3170
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
3171
+ const full = join(dir, entry);
3172
+ let stat;
3173
+ try {
3174
+ stat = statSync(full);
3175
+ } catch {
3176
+ continue;
3177
+ }
3178
+ if (stat.isDirectory()) {
3179
+ walkDir(full, out);
3180
+ } else if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(full)) {
3181
+ out.push(full);
3182
+ }
3183
+ }
3184
+ }
3185
+ function extractNodeIdsFromSource(source) {
3186
+ const nodeIds = /* @__PURE__ */ new Set();
3187
+ const re = new RegExp(NODE_ID_QUERY_RE, "g");
3188
+ let match;
3189
+ while ((match = re.exec(source)) !== null) {
3190
+ const raw = match[1];
3191
+ if (!raw) continue;
3192
+ const decoded = safeDecode(raw);
3193
+ nodeIds.add(decoded.replace(/-/g, ":"));
3194
+ }
3195
+ return nodeIds;
3196
+ }
3197
+ function safeDecode(raw) {
3198
+ try {
3199
+ return decodeURIComponent(raw);
3200
+ } catch {
3201
+ return raw;
3202
+ }
3203
+ }
3204
+ var PropertyAcknowledgmentIntentSchema = z.object({
3205
+ kind: z.literal("property").default("property"),
3206
+ field: z.string(),
3207
+ value: z.unknown(),
3208
+ scope: z.enum(["instance", "definition"])
3209
+ });
3210
+ var RuleOptOutAcknowledgmentIntentSchema = z.object({
3211
+ kind: z.literal("rule-opt-out"),
3212
+ ruleId: z.string()
3213
+ }).strict();
3214
+ var AcknowledgmentIntentSchema = z.preprocess((raw) => {
3215
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
3216
+ const obj = raw;
3217
+ if (obj["kind"] === void 0) {
3218
+ return { ...obj, kind: "property" };
3219
+ }
3220
+ }
3221
+ return raw;
3222
+ }, z.discriminatedUnion("kind", [
3223
+ PropertyAcknowledgmentIntentSchema,
3224
+ RuleOptOutAcknowledgmentIntentSchema
3225
+ ]));
3226
+ function isRuleOptOutIntent(intent) {
3227
+ return intent !== void 0 && intent.kind === "rule-opt-out";
3228
+ }
3229
+ var AcknowledgmentSceneWriteOutcomeSchema = z.object({
3230
+ result: z.enum([
3231
+ "succeeded",
3232
+ "silent-ignored",
3233
+ "api-rejected",
3234
+ "user-declined-propagation",
3235
+ "unknown"
3236
+ ]),
3237
+ reason: z.string().optional()
3238
+ });
3239
+ var AcknowledgmentSchema = z.object({
3240
+ nodeId: z.string(),
3241
+ ruleId: z.string(),
3242
+ intent: AcknowledgmentIntentSchema.optional(),
3243
+ sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
3244
+ codegenDirective: z.string().optional()
3245
+ });
3246
+ var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
3247
+ function normalizeNodeId(id) {
3248
+ return id.replace(/-/g, ":");
3249
+ }
3013
3250
 
3014
3251
  // src/core/rules/component/index.ts
3015
3252
  var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
@@ -3219,6 +3456,49 @@ defineRule({
3219
3456
  definition: variantStructureMismatchDef,
3220
3457
  check: variantStructureMismatchCheck
3221
3458
  });
3459
+ var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
3460
+ var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
3461
+ function codeConnectIsSetUp(context) {
3462
+ return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3463
+ return existsSync(join(process.cwd(), "figma.config.json"));
3464
+ });
3465
+ }
3466
+ function codeConnectMappings(context) {
3467
+ return getAnalysisState(
3468
+ context,
3469
+ CODE_CONNECT_MAPPINGS_KEY,
3470
+ () => parseCodeConnectMappings(process.cwd())
3471
+ );
3472
+ }
3473
+ var unmappedComponentDef = {
3474
+ id: "unmapped-component",
3475
+ name: "Unmapped Component",
3476
+ category: "code-quality",
3477
+ why: "Without a Code Connect mapping, figma-implement-design regenerates the same markup every time this component appears in a screen \u2014 wasting tokens and risking drift.",
3478
+ impact: "Future roundtrips on screens containing this component cannot reuse your existing code; they regenerate markup that may not match the canonical implementation.",
3479
+ fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
3480
+ };
3481
+ var unmappedComponentCheck = (node, context) => {
3482
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3483
+ if (isInsideInstance(context)) return null;
3484
+ if (!codeConnectIsSetUp(context)) return null;
3485
+ const mappings = codeConnectMappings(context);
3486
+ if (mappings.mappedNodeIds.has(node.id)) return null;
3487
+ const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
3488
+ if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
3489
+ return null;
3490
+ }
3491
+ return {
3492
+ ruleId: unmappedComponentDef.id,
3493
+ nodeId: node.id,
3494
+ nodePath: context.path.join(" > "),
3495
+ ...unmappedComponentMsg(node.name)
3496
+ };
3497
+ };
3498
+ defineRule({
3499
+ definition: unmappedComponentDef,
3500
+ check: unmappedComponentCheck
3501
+ });
3222
3502
 
3223
3503
  // src/core/rules/naming/index.ts
3224
3504
  function capitalize(s) {
@@ -3575,32 +3855,6 @@ defineRule({
3575
3855
  definition: missingPrototypeDef,
3576
3856
  check: missingPrototypeCheck
3577
3857
  });
3578
- var AcknowledgmentIntentSchema = z.object({
3579
- field: z.string(),
3580
- value: z.unknown(),
3581
- scope: z.enum(["instance", "definition"])
3582
- });
3583
- var AcknowledgmentSceneWriteOutcomeSchema = z.object({
3584
- result: z.enum([
3585
- "succeeded",
3586
- "silent-ignored",
3587
- "api-rejected",
3588
- "user-declined-propagation",
3589
- "unknown"
3590
- ]),
3591
- reason: z.string().optional()
3592
- });
3593
- var AcknowledgmentSchema = z.object({
3594
- nodeId: z.string(),
3595
- ruleId: z.string(),
3596
- intent: AcknowledgmentIntentSchema.optional(),
3597
- sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
3598
- codegenDirective: z.string().optional()
3599
- });
3600
- var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
3601
- function normalizeNodeId(id) {
3602
- return id.replace(/-/g, ":");
3603
- }
3604
3858
  var AnalysisScopeSchema = z.enum(["page", "component"]);
3605
3859
  var COMPONENT_SCOPE_ROOT_TYPES = /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET", "INSTANCE"]);
3606
3860
  function detectAnalysisScope(rootNode) {
@@ -3657,6 +3911,7 @@ var RuleEngine = class {
3657
3911
  excludeNamePattern;
3658
3912
  excludeNodeTypes;
3659
3913
  acknowledgments;
3914
+ acknowledgmentsByKey;
3660
3915
  scopeOverride;
3661
3916
  constructor(options = {}) {
3662
3917
  this.configs = options.configs ?? RULE_CONFIGS;
@@ -3665,10 +3920,15 @@ var RuleEngine = class {
3665
3920
  this.targetNodeId = options.targetNodeId;
3666
3921
  this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
3667
3922
  this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
3923
+ const ackList = options.acknowledgments ?? [];
3668
3924
  this.acknowledgments = new Set(
3669
- (options.acknowledgments ?? []).map(
3670
- (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
3671
- )
3925
+ ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
3926
+ );
3927
+ this.acknowledgmentsByKey = new Map(
3928
+ ackList.map((a) => [
3929
+ `${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
3930
+ a
3931
+ ])
3672
3932
  );
3673
3933
  this.scopeOverride = options.scope;
3674
3934
  }
@@ -3752,6 +4012,7 @@ var RuleEngine = class {
3752
4012
  if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
3753
4013
  return;
3754
4014
  }
4015
+ const acknowledgmentsByKey = this.acknowledgmentsByKey;
3755
4016
  const context = {
3756
4017
  file,
3757
4018
  parent,
@@ -3763,7 +4024,8 @@ var RuleEngine = class {
3763
4024
  siblings,
3764
4025
  analysisState,
3765
4026
  scope,
3766
- rootNodeType
4027
+ rootNodeType,
4028
+ findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
3767
4029
  };
3768
4030
  for (const rule of rules) {
3769
4031
  const ruleId = rule.definition.id;
@@ -4187,6 +4449,7 @@ var STRATEGY_BY_RULE = {
4187
4449
  // Strategy C — annotation only
4188
4450
  "absolute-position-in-auto-layout": "annotation",
4189
4451
  "variant-structure-mismatch": "annotation",
4452
+ "unmapped-component": "annotation",
4190
4453
  // Strategy D — auto-fix lower-severity issues from analyze output
4191
4454
  "non-standard-naming": "auto-fix",
4192
4455
  "inconsistent-naming-convention": "auto-fix",
@@ -4221,6 +4484,7 @@ function resolveTargetProperty(ruleId, subType) {
4221
4484
  case "raw-value":
4222
4485
  case "missing-interaction-state":
4223
4486
  case "missing-prototype":
4487
+ case "unmapped-component":
4224
4488
  return void 0;
4225
4489
  }
4226
4490
  }
@@ -4245,7 +4509,7 @@ function computeApplyContext(violation, instanceContext) {
4245
4509
  }
4246
4510
 
4247
4511
  // package.json
4248
- var version2 = "0.11.5";
4512
+ var version2 = "0.12.1";
4249
4513
 
4250
4514
  // src/core/engine/scoring.ts
4251
4515
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -4352,6 +4616,7 @@ function calculateScores(result, configs) {
4352
4616
  risk: 0,
4353
4617
  missingInfo: 0,
4354
4618
  suggestion: 0,
4619
+ note: 0,
4355
4620
  nodeCount,
4356
4621
  acknowledgedCount: 0
4357
4622
  };
@@ -4369,6 +4634,9 @@ function calculateScores(result, configs) {
4369
4634
  case "suggestion":
4370
4635
  summary.suggestion++;
4371
4636
  break;
4637
+ case "note":
4638
+ summary.note++;
4639
+ break;
4372
4640
  }
4373
4641
  if (issue.acknowledged === true) summary.acknowledgedCount++;
4374
4642
  }
@@ -4400,7 +4668,8 @@ function initializeCategoryScores() {
4400
4668
  blocking: 0,
4401
4669
  risk: 0,
4402
4670
  "missing-info": 0,
4403
- suggestion: 0
4671
+ suggestion: 0,
4672
+ note: 0
4404
4673
  }
4405
4674
  };
4406
4675
  }
@@ -4421,6 +4690,7 @@ function formatScoreSummary(report) {
4421
4690
  lines.push(` Risk: ${report.summary.risk}`);
4422
4691
  lines.push(` Missing Info: ${report.summary.missingInfo}`);
4423
4692
  lines.push(` Suggestion: ${report.summary.suggestion}`);
4693
+ lines.push(` Note: ${report.summary.note}`);
4424
4694
  if (report.summary.acknowledgedCount > 0) {
4425
4695
  const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
4426
4696
  lines.push(
@@ -4431,6 +4701,19 @@ function formatScoreSummary(report) {
4431
4701
  }
4432
4702
  return lines.join("\n");
4433
4703
  }
4704
+ function formatCodeConnectCoverageLine(coverage) {
4705
+ const { mapped, total } = coverage;
4706
+ const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
4707
+ return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
4708
+ }
4709
+ var ROUNDTRIP_OPT_OUT_HINT = "Some components may carry roundtrip-recorded opt-outs that this standalone analyze cannot see (Figma REST annotations field is in private beta). Run /canicode-roundtrip to apply opt-outs.";
4710
+ function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
4711
+ const hasUnmapped = issues.some(
4712
+ (issue) => issue.violation.ruleId === "unmapped-component"
4713
+ );
4714
+ if (!hasUnmapped) return null;
4715
+ return ROUNDTRIP_OPT_OUT_HINT;
4716
+ }
4434
4717
  function buildResultJson(fileName, result, scores, options) {
4435
4718
  const issuesByRule = {};
4436
4719
  for (const issue of result.issues) {
@@ -4460,6 +4743,14 @@ function buildResultJson(fileName, result, scores, options) {
4460
4743
  ...issue.acknowledged === true ? { acknowledged: true } : {}
4461
4744
  };
4462
4745
  });
4746
+ const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues) : null;
4747
+ const summaryParts = [formatScoreSummary(scores)];
4748
+ if (options?.codeConnectCoverage) {
4749
+ summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
4750
+ }
4751
+ if (optOutHint) {
4752
+ summaryParts.push(optOutHint);
4753
+ }
4463
4754
  const json = {
4464
4755
  version: version2,
4465
4756
  analyzedAt: result.analyzedAt,
@@ -4479,13 +4770,31 @@ function buildResultJson(fileName, result, scores, options) {
4479
4770
  },
4480
4771
  issuesByRule,
4481
4772
  issues,
4482
- summary: formatScoreSummary(scores)
4773
+ summary: summaryParts.join("\n\n")
4483
4774
  };
4775
+ if (options?.codeConnectCoverage) {
4776
+ json["codeConnectCoverage"] = options.codeConnectCoverage;
4777
+ }
4778
+ if (optOutHint) {
4779
+ json["roundtripOptOutHint"] = optOutHint;
4780
+ }
4484
4781
  if (result.failedRules.length > 0) {
4485
4782
  json["failedRules"] = result.failedRules;
4486
4783
  }
4487
4784
  return json;
4488
4785
  }
4786
+
4787
+ // src/core/rules/component/code-connect-coverage.ts
4788
+ function computeCodeConnectCoverage(components, cwd = process.cwd()) {
4789
+ const result = parseCodeConnectMappings(cwd);
4790
+ if (result.skipReason === "no-config") return void 0;
4791
+ const componentNodeIds = Object.keys(components);
4792
+ let mapped = 0;
4793
+ for (const nodeId of componentNodeIds) {
4794
+ if (result.mappedNodeIds.has(nodeId)) mapped++;
4795
+ }
4796
+ return { mapped, total: componentNodeIds.length };
4797
+ }
4489
4798
  function isFigmaUrl2(input) {
4490
4799
  return input.includes("figma.com/");
4491
4800
  }
@@ -4571,7 +4880,8 @@ function severityDot(sev) {
4571
4880
  blocking: "sev-blocking",
4572
4881
  risk: "sev-risk",
4573
4882
  "missing-info": "sev-missing",
4574
- suggestion: "sev-suggestion"
4883
+ suggestion: "sev-suggestion",
4884
+ note: "sev-note"
4575
4885
  };
4576
4886
  return map[sev];
4577
4887
  }
@@ -4580,7 +4890,8 @@ function severityBadge(sev) {
4580
4890
  blocking: "sev-blocking",
4581
4891
  risk: "sev-risk",
4582
4892
  "missing-info": "sev-missing",
4583
- suggestion: "sev-suggestion"
4893
+ suggestion: "sev-suggestion",
4894
+ note: "sev-note"
4584
4895
  };
4585
4896
  return map[sev];
4586
4897
  }
@@ -4638,6 +4949,7 @@ ${CATEGORIES.map((cat) => {
4638
4949
  ${renderSummaryDot("sev-risk", scores.summary.risk, "Risk")}
4639
4950
  ${renderSummaryDot("sev-missing", scores.summary.missingInfo, "Missing Info")}
4640
4951
  ${renderSummaryDot("sev-suggestion", scores.summary.suggestion, "Suggestion")}
4952
+ ${renderSummaryDot("sev-note", scores.summary.note, "Note")}
4641
4953
  <div class="rpt-summary-total">
4642
4954
  <span class="rpt-summary-count">${scores.summary.totalIssues}</span>
4643
4955
  <span class="rpt-summary-label">Total</span>
@@ -4852,7 +5164,7 @@ function groupIssuesByRule(issues) {
4852
5164
  group.issues.push(issue);
4853
5165
  group.totalScore += issue.calculatedScore;
4854
5166
  }
4855
- const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3 };
5167
+ const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3, note: 4 };
4856
5168
  return [...byRule.values()].sort((a, b) => {
4857
5169
  const sevDiff = (SEVERITY_RANK[a.severity] ?? 4) - (SEVERITY_RANK[b.severity] ?? 4);
4858
5170
  return sevDiff !== 0 ? sevDiff : a.totalScore - b.totalScore;
@@ -4920,6 +5232,7 @@ body {
4920
5232
  .sev-risk { background: var(--amber); }
4921
5233
  .sev-missing { background: #a1a1aa; }
4922
5234
  .sev-suggestion { background: var(--green); }
5235
+ .sev-note { background: #d4d4d8; }
4923
5236
 
4924
5237
  /* ---- Print ---- */
4925
5238
  @media print {
@@ -5316,6 +5629,7 @@ body {
5316
5629
  .rpt-issue-score.sev-risk { background: var(--amber-bg); color: #d97706; border-color: rgba(245,158,11,0.2); }
5317
5630
  .rpt-issue-score.sev-missing { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
5318
5631
  .rpt-issue-score.sev-suggestion { background: var(--green-bg); color: #16a34a; border-color: rgba(34,197,94,0.2); }
5632
+ .rpt-issue-score.sev-note { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
5319
5633
 
5320
5634
  .rpt-issue-body {
5321
5635
  padding: 12px;
@@ -5741,8 +6055,8 @@ var AnalyzeOptionsSchema = z.object({
5741
6055
  function registerAnalyze(cli2) {
5742
6056
  cli2.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(
5743
6057
  "--api",
5744
- "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `gotcha-survey` (#461)."
5745
- ).option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").option("--json", "Output JSON results to stdout (same format as MCP)").option("--acknowledgments <path>", "(#371 / ADR-019) Path to JSON acknowledgments from canicode Figma annotations (nodeId, ruleId; optional intent / sceneWriteOutcome / codegenDirective per #444). Matching issues are flagged acknowledged and contribute half weight to density.").option("--scope <scope>", "(#404) Override analysis scope: `page` (screen/section \u2014 container bounds are required) or `component` (standalone reusable unit \u2014 root FILL is the design contract). Defaults to auto-detection from the root node type.").option("--ready-min-grade <grade>", "Minimum grade for code-gen readiness (S | A+ | A | B+ | B | C+ | C | D | F). Overrides configPath codegenReadyMinGrade. Default: A").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze ./fixtures/my-design --output report.html").example(" canicode analyze ./fixtures/my-design --config ./my-config.json").action(async (input, rawOptions) => {
6058
+ "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `gotcha-survey`."
6059
+ ).option("--screenshot", "Include screenshot comparison in report (requires ANTHROPIC_API_KEY)").option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--no-open", "Don't open report in browser after analysis").option("--json", "Output JSON results to stdout (same format as MCP)").option("--acknowledgments <path>", "Path to JSON acknowledgments from canicode Figma annotations (nodeId, ruleId; optional intent / sceneWriteOutcome / codegenDirective). Matching issues are flagged acknowledged and contribute half weight to density.").option("--scope <scope>", "Override analysis scope: `page` (screen/section \u2014 container bounds are required) or `component` (standalone reusable unit \u2014 root FILL is the design contract). Defaults to auto-detection from the root node type.").option("--ready-min-grade <grade>", "Minimum grade for code-gen readiness (S | A+ | A | B+ | B | C+ | C | D | F). Overrides configPath codegenReadyMinGrade. Default: A").example(" canicode analyze https://www.figma.com/design/ABC123/MyDesign").example(" canicode analyze ./fixtures/my-design --output report.html").example(" canicode analyze ./fixtures/my-design --config ./my-config.json").action(async (input, rawOptions) => {
5746
6060
  const parseResult = AnalyzeOptionsSchema.safeParse(rawOptions);
5747
6061
  if (!parseResult.success) {
5748
6062
  const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
@@ -5839,8 +6153,17 @@ Analyzing: ${file.name}`);
5839
6153
  const result = analyzeFile(file, analyzeOptions);
5840
6154
  log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
5841
6155
  const scores = calculateScores(result, configs);
6156
+ const coverage = computeCodeConnectCoverage(file.components);
6157
+ const optOutHintEligible = acknowledgments === void 0;
6158
+ const optOutHint = optOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
5842
6159
  if (options.json) {
5843
- console.log(JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input), ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {} }), null, 2));
6160
+ console.log(JSON.stringify(buildResultJson(file.name, result, scores, {
6161
+ fileKey: file.fileKey,
6162
+ designKey: computeDesignKey(input),
6163
+ ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {},
6164
+ ...coverage ? { codeConnectCoverage: coverage } : {},
6165
+ roundtripOptOutHintEligible: optOutHintEligible
6166
+ }), null, 2));
5844
6167
  if (scores.overall.grade === "F") {
5845
6168
  process.exitCode = 1;
5846
6169
  }
@@ -5848,6 +6171,14 @@ Analyzing: ${file.name}`);
5848
6171
  }
5849
6172
  console.log("\n" + "=".repeat(50));
5850
6173
  console.log(formatScoreSummary(scores));
6174
+ if (coverage) {
6175
+ console.log("");
6176
+ console.log(formatCodeConnectCoverageLine(coverage));
6177
+ }
6178
+ if (optOutHint) {
6179
+ console.log("");
6180
+ console.log(optOutHint);
6181
+ }
5851
6182
  console.log("=".repeat(50));
5852
6183
  const now = /* @__PURE__ */ new Date();
5853
6184
  const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}`;
@@ -5961,6 +6292,12 @@ var GOTCHA_QUESTION_CONTENT = {
5961
6292
  hint: "Describe which variant has the correct structure, or if they should all match",
5962
6293
  example: "Default variant is canonical \u2014 other variants should toggle child visibility instead of adding/removing elements"
5963
6294
  },
6295
+ "unmapped-component": {
6296
+ ruleId: "unmapped-component",
6297
+ question: '"{nodeName}" has no Code Connect mapping yet. Should we register one so figma-implement-design reuses your code?',
6298
+ hint: "Skip if this component is intentionally unmapped (e.g. marketing-only banner). Otherwise run /canicode-roundtrip on the component to walk through registration.",
6299
+ example: "Yes \u2014 map to src/components/Button.tsx so future screens reuse the existing implementation"
6300
+ },
5964
6301
  "deep-nesting": {
5965
6302
  ruleId: "deep-nesting",
5966
6303
  question: '"{nodeName}" is deeply nested. Can some intermediate layers be flattened or extracted?',
@@ -6106,10 +6443,7 @@ function generateGotchaSurvey(result, scores, options = {}) {
6106
6443
  const relevantIssues = result.issues.filter((issue) => {
6107
6444
  const severity = issue.config.severity;
6108
6445
  if (severity === "blocking" || severity === "risk") return true;
6109
- if (severity === "missing-info") {
6110
- return getRulePurpose(issue.violation.ruleId) === "info-collection";
6111
- }
6112
- return false;
6446
+ return getRulePurpose(issue.violation.ruleId) === "info-collection";
6113
6447
  });
6114
6448
  const deduped = deduplicateSiblingIssues(relevantIssues);
6115
6449
  const sorted = stableSortBySeverity(deduped);
@@ -6322,8 +6656,8 @@ function formatHumanSummary(survey) {
6322
6656
  function registerGotchaSurvey(cli2) {
6323
6657
  cli2.command("gotcha-survey <input>", "Generate a gotcha survey from a Figma design analysis").option("--preset <preset>", "Analysis preset (relaxed | dev-friendly | ai-ready | strict)").option("--token <token>", "Figma API token (or use FIGMA_TOKEN env var)").option(
6324
6658
  "--api",
6325
- "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `analyze` (#461)."
6326
- ).option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--target-node-id <id>", "Scope analysis to a specific node ID").option("--scope <scope>", "(#404) Override analysis scope: `page` or `component`. Defaults to auto-detection from the root node type.").option("--json", "Output GotchaSurvey JSON to stdout (same format as MCP)").option("--ready-min-grade <grade>", "Minimum grade for code-gen readiness (S | A+ | A | B+ | B | C+ | C | D | F). Overrides configPath codegenReadyMinGrade. Default: A").example(" canicode gotcha-survey https://www.figma.com/design/ABC123/MyDesign --json").example(" canicode gotcha-survey ./fixtures/my-design --json").action(async (input, rawOptions) => {
6659
+ "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `analyze`."
6660
+ ).option("--config <path>", "Path to config JSON file (override rule scores/settings)").option("--target-node-id <id>", "Scope analysis to a specific node ID").option("--scope <scope>", "Override analysis scope: `page` or `component`. Defaults to auto-detection from the root node type.").option("--json", "Output GotchaSurvey JSON to stdout (same format as MCP)").option("--ready-min-grade <grade>", "Minimum grade for code-gen readiness (S | A+ | A | B+ | B | C+ | C | D | F). Overrides configPath codegenReadyMinGrade. Default: A").example(" canicode gotcha-survey https://www.figma.com/design/ABC123/MyDesign --json").example(" canicode gotcha-survey ./fixtures/my-design --json").action(async (input, rawOptions) => {
6327
6661
  const parseResult = GotchaSurveyOptionsSchema.safeParse(rawOptions);
6328
6662
  if (!parseResult.success) {
6329
6663
  const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
@@ -6699,8 +7033,8 @@ function renderUpsertedFile(args) {
6699
7033
  }
6700
7034
  let working = currentContent;
6701
7035
  if (state === "missing-heading") {
6702
- const sep = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
6703
- working = `${working}${sep}${COLLECTED_GOTCHAS_HEADING}
7036
+ const sep2 = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
7037
+ working = `${working}${sep2}${COLLECTED_GOTCHAS_HEADING}
6704
7038
  `;
6705
7039
  }
6706
7040
  const plan = findOrAppendSection(working, designKey);
@@ -6748,7 +7082,7 @@ var UpsertOptionsSchema = z.object({
6748
7082
  });
6749
7083
  var USER_MESSAGES = {
6750
7084
  missing: "Gotchas SKILL.md not found at the given path. Run `canicode init` first, then re-invoke this skill.",
6751
- clobbered: "Your gotchas SKILL.md is missing the canicode YAML frontmatter (pre-#340 single-design clobber). Run `canicode init --force` to restore the workflow, then re-run this survey."
7085
+ clobbered: "Your gotchas SKILL.md is missing the canicode YAML frontmatter. Run `canicode init --force` to restore the workflow, then re-run this survey."
6752
7086
  };
6753
7087
  async function readStdin() {
6754
7088
  const chunks = [];
@@ -7189,6 +7523,47 @@ async function promptOverwriteBatch(candidates) {
7189
7523
  }
7190
7524
  return decisions;
7191
7525
  }
7526
+ var NonInteractiveError = class extends Error {
7527
+ constructor(message = "Interactive prompt requires a TTY") {
7528
+ super(message);
7529
+ this.name = "NonInteractiveError";
7530
+ }
7531
+ };
7532
+ async function promptForFigmaToken(opts = {}) {
7533
+ const isTTY = opts.isTTY ?? process.stdin.isTTY ?? false;
7534
+ if (!isTTY) {
7535
+ throw new NonInteractiveError();
7536
+ }
7537
+ const input = opts.input ?? process.stdin;
7538
+ const output = opts.output ?? process.stdout;
7539
+ const maxAttempts = opts.maxAttempts ?? 3;
7540
+ const rl = readline.createInterface({ input, output });
7541
+ try {
7542
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
7543
+ const answer = (await rl.question("Figma token: ")).trim();
7544
+ if (answer.length > 0) {
7545
+ return answer;
7546
+ }
7547
+ if (attempt < maxAttempts) {
7548
+ output.write("Token cannot be empty. Try again.\n");
7549
+ }
7550
+ }
7551
+ throw new Error(`No token provided after ${maxAttempts} attempts`);
7552
+ } finally {
7553
+ rl.close();
7554
+ }
7555
+ }
7556
+ function maskFigmaToken(token) {
7557
+ if (!token) return "(empty)";
7558
+ const BULLETS = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
7559
+ if (token.startsWith("figd_") && token.length > 9) {
7560
+ return `figd_${BULLETS}${token.slice(-4)}`;
7561
+ }
7562
+ if (token.length >= 4) {
7563
+ return `${BULLETS}${token.slice(-4)}`;
7564
+ }
7565
+ return "\u2022".repeat(token.length);
7566
+ }
7192
7567
 
7193
7568
  // src/cli/commands/init.ts
7194
7569
  function figmaEntryInMcpFile(filePath) {
@@ -7217,7 +7592,7 @@ function formatNextSteps(opts) {
7217
7592
  " Next:",
7218
7593
  " 1. Restart Cursor or reload MCP (so skills + MCP tools load in a fresh session)",
7219
7594
  " 2. In Agent chat, @ canicode-roundtrip with your Figma URL (or @ canicode-gotchas for survey-only)",
7220
- " \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode (project `.cursor/mcp.json`), then reload MCP; otherwise skills keep using `npx canicode \u2026` (#433)."
7595
+ " \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode (project `.cursor/mcp.json`), then reload MCP; otherwise skills keep using `npx canicode \u2026`."
7221
7596
  ].join("\n");
7222
7597
  }
7223
7598
  return [
@@ -7225,7 +7600,7 @@ function formatNextSteps(opts) {
7225
7600
  " Next:",
7226
7601
  " 1. Restart Claude Code (the newly installed skills only load on a fresh session)",
7227
7602
  " 2. Run /canicode-roundtrip <figma-url>",
7228
- " \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so `analyze` / `gotcha-survey` tools load \u2014 otherwise skills shell out to `npx canicode \u2026` (#433)."
7603
+ " \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so `analyze` / `gotcha-survey` tools load \u2014 otherwise skills shell out to `npx canicode \u2026`."
7229
7604
  ].join("\n");
7230
7605
  }
7231
7606
  if (cursor) {
@@ -7235,7 +7610,7 @@ function formatNextSteps(opts) {
7235
7610
  " 1. Add Figma MCP to .cursor/mcp.json (see https://github.com/let-sunny/canicode/blob/main/docs/CUSTOMIZATION.md#cursor-mcp-canicode and Figma MCP docs)",
7236
7611
  " 2. Restart Cursor so Figma tools (e.g. use_figma) load",
7237
7612
  " 3. @ canicode-roundtrip with your Figma URL for full roundtrip",
7238
- " \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per the Customization guide (`#cursor-mcp-canicode`), then reload MCP; otherwise skills keep using `npx canicode \u2026` (#433)."
7613
+ " \u2022 Optional \u2014 faster canicode MCP than `npx`: add `canicode-mcp` per the Customization guide (`#cursor-mcp-canicode`), then reload MCP; otherwise skills keep using `npx canicode \u2026`."
7239
7614
  ].join("\n");
7240
7615
  }
7241
7616
  return [
@@ -7245,7 +7620,7 @@ function formatNextSteps(opts) {
7245
7620
  " claude mcp add -s project -t http figma https://mcp.figma.com/mcp",
7246
7621
  " 2. Restart Claude Code (so the new skills + Figma MCP tools both load)",
7247
7622
  " 3. Run /canicode-roundtrip <figma-url>",
7248
- " \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so MCP tools load \u2014 otherwise skills shell out to `npx canicode \u2026` (#433)."
7623
+ " \u2022 Optional \u2014 faster canicode MCP than `npx`: `claude mcp add canicode -- npx --yes --package=canicode canicode-mcp`, then restart Claude Code so MCP tools load \u2014 otherwise skills shell out to `npx canicode \u2026`."
7249
7624
  ].join("\n");
7250
7625
  }
7251
7626
  var InitOptionsSchema = z.object({
@@ -7258,6 +7633,56 @@ var InitOptionsSchema = z.object({
7258
7633
  function wantsSkillInstallWithoutToken(options) {
7259
7634
  return options.cursorSkills === true;
7260
7635
  }
7636
+ async function runFullInit(token, options, interactive) {
7637
+ initAiready(token);
7638
+ console.log(` Config saved: ${getConfigPath()}`);
7639
+ console.log(` Reports will be saved to: ${getReportsDir()}/`);
7640
+ const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
7641
+ trackEvent(EVENTS.CLI_INIT, {
7642
+ skillsRequested: true,
7643
+ cursorSkillsRequested: options.cursorSkills === true,
7644
+ skillStepOk,
7645
+ target: options.global ? "global" : "project",
7646
+ force: options.force ?? false,
7647
+ interactive,
7648
+ ...skillSummary ?? {}
7649
+ });
7650
+ if (skillStepOk) {
7651
+ console.log(
7652
+ formatNextSteps({
7653
+ figmaMcpPresent: figmaMcpRegistered(),
7654
+ skillsInstalled: true,
7655
+ cursorSkillsInstalled: options.cursorSkills === true
7656
+ })
7657
+ );
7658
+ }
7659
+ }
7660
+ function printSetupGuide() {
7661
+ console.log(`CANICODE SETUP
7662
+ `);
7663
+ console.log(
7664
+ ` Never paste your token into Claude/Cursor chat \u2014 use FIGMA_TOKEN=\u2026 npx canicode init or this prompt only.
7665
+ `
7666
+ );
7667
+ console.log(` canicode init (interactive prompt; TTY only)`);
7668
+ console.log(` canicode init --token YOUR_FIGMA_TOKEN (CI / non-TTY)`);
7669
+ console.log(` Get token: figma.com > Settings > Personal access tokens
7670
+ `);
7671
+ console.log(`Skills:`);
7672
+ console.log(` --token (or the interactive prompt) also installs three Claude Code skills`);
7673
+ console.log(` into ./.claude/skills/ (canicode, canicode-gotchas, canicode-roundtrip).`);
7674
+ console.log(` --global Install to ~/.claude/skills/ instead`);
7675
+ console.log(` --cursor-skills Install Claude skills under .claude/skills/ plus Cursor copies under .cursor/skills/ (no --token yet \u2014 add --token when ready for REST analyze)`);
7676
+ console.log(` --force Overwrite existing skill files without prompting
7677
+ `);
7678
+ console.log(`Manage saved config (no skill reinstall):`);
7679
+ console.log(` canicode config set-token Rotate saved Figma token`);
7680
+ console.log(` canicode config show Print masked token + paths`);
7681
+ console.log(` canicode config path Print absolute config path
7682
+ `);
7683
+ console.log(`After setup:`);
7684
+ console.log(` canicode analyze "https://www.figma.com/design/..."`);
7685
+ }
7261
7686
  async function runInitSkillInstallSteps(options) {
7262
7687
  let skillStepOk = true;
7263
7688
  let skillSummary;
@@ -7331,27 +7756,7 @@ ${msg}`);
7331
7756
  }
7332
7757
  const options = parseResult.data;
7333
7758
  if (options.token) {
7334
- initAiready(options.token);
7335
- console.log(` Config saved: ${getConfigPath()}`);
7336
- console.log(` Reports will be saved to: ${getReportsDir()}/`);
7337
- const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
7338
- trackEvent(EVENTS.CLI_INIT, {
7339
- skillsRequested: true,
7340
- cursorSkillsRequested: options.cursorSkills === true,
7341
- skillStepOk,
7342
- target: options.global ? "global" : "project",
7343
- force: options.force ?? false,
7344
- ...skillSummary ?? {}
7345
- });
7346
- if (skillStepOk) {
7347
- console.log(
7348
- formatNextSteps({
7349
- figmaMcpPresent: figmaMcpRegistered(),
7350
- skillsInstalled: true,
7351
- cursorSkillsInstalled: options.cursorSkills === true
7352
- })
7353
- );
7354
- }
7759
+ await runFullInit(options.token, options, false);
7355
7760
  return;
7356
7761
  }
7357
7762
  if (wantsSkillInstallWithoutToken(options)) {
@@ -7379,24 +7784,16 @@ ${msg}`);
7379
7784
  }
7380
7785
  return;
7381
7786
  }
7382
- console.log(`CANICODE SETUP
7383
- `);
7384
- console.log(
7385
- ` Never paste your token into Claude/Cursor chat \u2014 use FIGMA_TOKEN=\u2026 npx canicode init or this prompt only.
7386
- `
7387
- );
7388
- console.log(` canicode init --token YOUR_FIGMA_TOKEN`);
7389
- console.log(` Get token: figma.com > Settings > Personal access tokens
7390
- `);
7391
- console.log(`Skills:`);
7392
- console.log(` --token also installs three Claude Code skills into ./.claude/skills/`);
7393
- console.log(` (canicode, canicode-gotchas, canicode-roundtrip).`);
7394
- console.log(` --global Install to ~/.claude/skills/ instead`);
7395
- console.log(` --cursor-skills Install Claude skills under .claude/skills/ plus Cursor copies under .cursor/skills/ (no --token yet \u2014 add --token when ready for REST analyze)`);
7396
- console.log(` --force Overwrite existing skill files without prompting
7397
- `);
7398
- console.log(`After setup:`);
7399
- console.log(` canicode analyze "https://www.figma.com/design/..."`);
7787
+ try {
7788
+ const token = await promptForFigmaToken();
7789
+ await runFullInit(token, options, true);
7790
+ return;
7791
+ } catch (promptError) {
7792
+ if (!(promptError instanceof NonInteractiveError)) {
7793
+ throw promptError;
7794
+ }
7795
+ }
7796
+ printSetupGuide();
7400
7797
  } catch (error) {
7401
7798
  console.error(
7402
7799
  "\nError:",
@@ -7408,28 +7805,85 @@ ${msg}`);
7408
7805
  }
7409
7806
 
7410
7807
  // src/cli/commands/config.ts
7808
+ var VALID_ACTIONS = ["set-token", "show", "path"];
7809
+ function isConfigAction(value) {
7810
+ return value !== void 0 && VALID_ACTIONS.includes(value);
7811
+ }
7812
+ function printConfigShow() {
7813
+ const cfg = readConfig();
7814
+ const envToken = process.env["FIGMA_TOKEN"];
7815
+ const effectiveToken = getFigmaToken();
7816
+ const tokenSource = envToken ? " (env: FIGMA_TOKEN)" : "";
7817
+ console.log("CANICODE CONFIG\n");
7818
+ console.log(` Config path: ${getConfigPath()}`);
7819
+ console.log(` Reports dir: ${getReportsDir()}`);
7820
+ console.log(` Figma token: ${maskFigmaToken(effectiveToken)}${tokenSource}`);
7821
+ console.log(` Telemetry: ${cfg.telemetry !== false ? "enabled" : "disabled"}`);
7822
+ console.log(`
7823
+ Options:`);
7824
+ console.log(` canicode config set-token Update saved Figma token (no skill reinstall)`);
7825
+ console.log(` canicode config show Show current configuration`);
7826
+ console.log(` canicode config path Print absolute config path`);
7827
+ console.log(` canicode config --no-telemetry Opt out of anonymous telemetry`);
7828
+ console.log(` canicode config --telemetry Opt back in`);
7829
+ }
7830
+ async function handleSetToken(options) {
7831
+ let token = options.token;
7832
+ const usedFlag = Boolean(token);
7833
+ if (!token) {
7834
+ try {
7835
+ token = await promptForFigmaToken();
7836
+ } catch (err) {
7837
+ if (err instanceof NonInteractiveError) {
7838
+ console.error(
7839
+ "Run with --token <token> or set FIGMA_TOKEN=\u2026 (interactive prompt requires a TTY)."
7840
+ );
7841
+ process.exitCode = 1;
7842
+ return;
7843
+ }
7844
+ throw err;
7845
+ }
7846
+ }
7847
+ setFigmaToken(token);
7848
+ console.log(`Token saved: ${getConfigPath()}`);
7849
+ trackEvent(EVENTS.CLI_CONFIG_SET_TOKEN, { interactive: !usedFlag });
7850
+ }
7411
7851
  function registerConfig(cli2) {
7412
- cli2.command("config", "Manage canicode configuration").option("--telemetry", "Enable anonymous telemetry").option("--no-telemetry", "Disable anonymous telemetry").action((options) => {
7852
+ cli2.command("config [action]", "Manage canicode configuration (actions: set-token, show, path)").option("--telemetry", "Enable anonymous telemetry").option("--no-telemetry", "Disable anonymous telemetry").option("--token <token>", "For `config set-token`: set token non-interactively (CI / non-TTY)").action(async (action, options) => {
7413
7853
  try {
7414
- if (options.telemetry === false) {
7854
+ if (action !== void 0 && !isConfigAction(action)) {
7855
+ console.error(
7856
+ `Unknown config action: ${action}. Available: ${VALID_ACTIONS.join(", ")}`
7857
+ );
7858
+ process.exitCode = 1;
7859
+ return;
7860
+ }
7861
+ if (action === "set-token") {
7862
+ await handleSetToken(options);
7863
+ return;
7864
+ }
7865
+ if (action === "path") {
7866
+ console.log(getConfigPath());
7867
+ return;
7868
+ }
7869
+ if (action === "show") {
7870
+ printConfigShow();
7871
+ return;
7872
+ }
7873
+ const argv = process.argv.slice(2);
7874
+ const flippedOff = argv.includes("--no-telemetry");
7875
+ const flippedOn = argv.includes("--telemetry");
7876
+ if (flippedOff) {
7415
7877
  setTelemetryEnabled(false);
7416
7878
  console.log("Telemetry disabled. No analytics data will be sent.");
7417
7879
  return;
7418
7880
  }
7419
- if (options.telemetry === true) {
7881
+ if (flippedOn) {
7420
7882
  setTelemetryEnabled(true);
7421
7883
  console.log("Telemetry enabled. Only anonymous usage events are tracked \u2014 no design data.");
7422
7884
  return;
7423
7885
  }
7424
- const cfg = readConfig();
7425
- console.log("CANICODE CONFIG\n");
7426
- console.log(` Config path: ${getConfigPath()}`);
7427
- console.log(` Figma token: ${cfg.figmaToken ? "set" : "not set"}`);
7428
- console.log(` Telemetry: ${cfg.telemetry !== false ? "enabled" : "disabled"}`);
7429
- console.log(`
7430
- Options:`);
7431
- console.log(` canicode config --no-telemetry Opt out of anonymous telemetry`);
7432
- console.log(` canicode config --telemetry Opt back in`);
7886
+ printConfigShow();
7433
7887
  } catch (error) {
7434
7888
  console.error(
7435
7889
  "\nError:",
@@ -7440,6 +7894,189 @@ Options:`);
7440
7894
  });
7441
7895
  }
7442
7896
 
7897
+ // src/cli/commands/doctor.ts
7898
+ init_figma_client();
7899
+ var CODE_CONNECT_PKG = "@figma/code-connect";
7900
+ var CODE_CONNECT_DOCS = "https://www.figma.com/code-connect-docs/";
7901
+ function readPackageJson(cwd) {
7902
+ const pkgPath = join(cwd, "package.json");
7903
+ if (!existsSync(pkgPath)) return void 0;
7904
+ try {
7905
+ return JSON.parse(readFileSync(pkgPath, "utf-8"));
7906
+ } catch {
7907
+ return void 0;
7908
+ }
7909
+ }
7910
+ function findCodeConnectVersion(pkg3) {
7911
+ if (!pkg3) return void 0;
7912
+ return pkg3.dependencies?.[CODE_CONNECT_PKG] ?? pkg3.devDependencies?.[CODE_CONNECT_PKG];
7913
+ }
7914
+ function runCodeConnectChecks(cwd) {
7915
+ const pkg3 = readPackageJson(cwd);
7916
+ const ccVersion = findCodeConnectVersion(pkg3);
7917
+ const figmaConfigExists = existsSync(join(cwd, "figma.config.json"));
7918
+ const results = [];
7919
+ if (ccVersion) {
7920
+ results.push({
7921
+ name: `${CODE_CONNECT_PKG} installed`,
7922
+ pass: true,
7923
+ detail: ccVersion
7924
+ });
7925
+ } else {
7926
+ results.push({
7927
+ name: `${CODE_CONNECT_PKG} not installed`,
7928
+ pass: false,
7929
+ remediation: pkg3 ? `pnpm add -D ${CODE_CONNECT_PKG} (or npm/yarn equivalent)` : `No package.json found at ${cwd} \u2014 run from your project root, or initialise one first.`
7930
+ });
7931
+ }
7932
+ if (figmaConfigExists) {
7933
+ results.push({
7934
+ name: "figma.config.json found at repo root",
7935
+ pass: true
7936
+ });
7937
+ } else {
7938
+ results.push({
7939
+ name: "figma.config.json not found at repo root",
7940
+ pass: false,
7941
+ remediation: `see ${CODE_CONNECT_DOCS}`
7942
+ });
7943
+ }
7944
+ return results;
7945
+ }
7946
+ var PUBLISH_CHECK_NAME = "Figma component published in a library";
7947
+ async function runFigmaPublishCheck(input) {
7948
+ const { figmaUrl, token, fetchPublishedComponents } = input;
7949
+ let parsed;
7950
+ try {
7951
+ parsed = parseFigmaUrl(figmaUrl);
7952
+ } catch (err) {
7953
+ const message = err instanceof FigmaUrlParseError ? err.message : String(err);
7954
+ return {
7955
+ name: PUBLISH_CHECK_NAME,
7956
+ pass: false,
7957
+ inconclusive: true,
7958
+ detail: `could not parse URL: ${message}`,
7959
+ remediation: "Pass a valid Figma design URL (figma.com/design/<file>?node-id=<id>)."
7960
+ };
7961
+ }
7962
+ if (!parsed.nodeId) {
7963
+ return {
7964
+ name: PUBLISH_CHECK_NAME,
7965
+ pass: false,
7966
+ inconclusive: true,
7967
+ detail: "URL is missing a node-id",
7968
+ remediation: "Code Connect mapping is per-component \u2014 invoke with a URL that targets a specific node (?node-id=\u2026)."
7969
+ };
7970
+ }
7971
+ if (!token) {
7972
+ return {
7973
+ name: PUBLISH_CHECK_NAME,
7974
+ pass: false,
7975
+ inconclusive: true,
7976
+ detail: "FIGMA_TOKEN not configured \u2014 skipping publish-status check",
7977
+ remediation: "Set FIGMA_TOKEN (env var) or run `canicode config set-token` so doctor can verify this prereq inline."
7978
+ };
7979
+ }
7980
+ if (!fetchPublishedComponents) {
7981
+ return {
7982
+ name: PUBLISH_CHECK_NAME,
7983
+ pass: false,
7984
+ inconclusive: true,
7985
+ detail: "no fetcher wired",
7986
+ remediation: "internal: doctor was called without a Figma client"
7987
+ };
7988
+ }
7989
+ let components;
7990
+ try {
7991
+ components = await fetchPublishedComponents(parsed.fileKey);
7992
+ } catch (err) {
7993
+ const status = err instanceof FigmaClientError ? err.statusCode : void 0;
7994
+ const message = err instanceof Error ? err.message : String(err);
7995
+ return {
7996
+ name: PUBLISH_CHECK_NAME,
7997
+ pass: false,
7998
+ inconclusive: true,
7999
+ detail: `Figma API call failed${status ? ` (HTTP ${status})` : ""}: ${message}`,
8000
+ remediation: "Step 7d will rely on the API as the authority; if your token / network is OK, the canicode-roundtrip step itself will surface the publish error inline."
8001
+ };
8002
+ }
8003
+ const canonicalNodeId = parsed.nodeId.replace(/-/g, ":");
8004
+ const match = components.find(
8005
+ (c) => c.node_id === canonicalNodeId || c.node_id === parsed.nodeId
8006
+ );
8007
+ if (match) {
8008
+ return {
8009
+ name: PUBLISH_CHECK_NAME,
8010
+ pass: true,
8011
+ detail: `${match.name} (${match.node_id})`
8012
+ };
8013
+ }
8014
+ return {
8015
+ name: PUBLISH_CHECK_NAME,
8016
+ pass: false,
8017
+ detail: `node ${canonicalNodeId} is not in the published-components list for file ${parsed.fileKey}`,
8018
+ remediation: "Open the file in Figma \u2192 Assets panel \u2192 Publish library and include this component. Without publishing, `add_code_connect_map` fails with 'Published component not found.'"
8019
+ };
8020
+ }
8021
+ function formatDoctorReport(results) {
8022
+ const lines = ["Code Connect"];
8023
+ for (const result of results) {
8024
+ const icon = result.pass ? "\u2705" : result.inconclusive ? "\u26A0\uFE0F" : "\u274C";
8025
+ const detail = result.detail ? ` (${result.detail})` : "";
8026
+ lines.push(` ${icon} ${result.name}${detail}`);
8027
+ if (!result.pass && result.remediation) {
8028
+ lines.push(` \u2192 ${result.remediation}`);
8029
+ }
8030
+ }
8031
+ lines.push("");
8032
+ const blocking = results.filter((r) => !r.pass && !r.inconclusive).length;
8033
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8034
+ if (blocking === 0 && inconclusive === 0) {
8035
+ lines.push("All checks passed.");
8036
+ } else if (blocking === 0) {
8037
+ lines.push(
8038
+ "Blocking checks passed; some checks were skipped (\u26A0\uFE0F) and could not be verified."
8039
+ );
8040
+ } else {
8041
+ lines.push("Some checks failed. Fix the items above before running the Code Connect flow.");
8042
+ }
8043
+ return lines.join("\n");
8044
+ }
8045
+ function registerDoctor(cli2) {
8046
+ cli2.command(
8047
+ "doctor",
8048
+ "Diagnose Code Connect prerequisites (`@figma/code-connect`, `figma.config.json`)"
8049
+ ).option(
8050
+ "--figma-url <url>",
8051
+ "Optionally check that the target Figma component is published in a library (requires FIGMA_TOKEN)"
8052
+ ).action(async (options) => {
8053
+ const cwd = process.cwd();
8054
+ const results = runCodeConnectChecks(cwd);
8055
+ if (options.figmaUrl) {
8056
+ const token = getFigmaToken();
8057
+ const client = token ? new FigmaClient({ token }) : void 0;
8058
+ const publishCheck = await runFigmaPublishCheck({
8059
+ figmaUrl: options.figmaUrl,
8060
+ token,
8061
+ fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0
8062
+ });
8063
+ results.push(publishCheck);
8064
+ }
8065
+ console.log(formatDoctorReport(results));
8066
+ const passed = results.filter((r) => r.pass).length;
8067
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8068
+ const failed = results.length - passed - inconclusive;
8069
+ trackEvent(EVENTS.CLI_DOCTOR, {
8070
+ passed,
8071
+ failed,
8072
+ total: results.length
8073
+ });
8074
+ if (failed > 0) {
8075
+ process.exitCode = 1;
8076
+ }
8077
+ });
8078
+ }
8079
+
7443
8080
  // src/cli/commands/list-rules.ts
7444
8081
  function registerListRules(cli2) {
7445
8082
  cli2.command("list-rules", "List all analysis rules with scores and severity").option("--config <path>", "Apply config overrides to show effective scores").option("--json", "Output as JSON").action(async (options) => {
@@ -10566,8 +11203,8 @@ ${msg}`);
10566
11203
  if (vectorDir && existsSync(vectorDir)) {
10567
11204
  const vecOutputDir = resolve(outputDir, "vectors");
10568
11205
  mkdirSync(vecOutputDir, { recursive: true });
10569
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10570
- const vecFiles = readdirSync4(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
11206
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11207
+ const vecFiles = readdirSync5(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
10571
11208
  for (const f of vecFiles) {
10572
11209
  copyFileSync3(resolve(vectorDir, f), resolve(vecOutputDir, f));
10573
11210
  }
@@ -10577,8 +11214,8 @@ ${msg}`);
10577
11214
  if (imageDir && existsSync(imageDir)) {
10578
11215
  const imgOutputDir = resolve(outputDir, "images");
10579
11216
  mkdirSync(imgOutputDir, { recursive: true });
10580
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10581
- const imgFiles = readdirSync4(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
11217
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11218
+ const imgFiles = readdirSync5(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
10582
11219
  for (const f of imgFiles) {
10583
11220
  copyFileSync3(resolve(imageDir, f), resolve(imgOutputDir, f));
10584
11221
  }
@@ -10786,6 +11423,7 @@ registerDesignTree(cli);
10786
11423
  registerVisualCompare(cli);
10787
11424
  registerInit(cli);
10788
11425
  registerConfig(cli);
11426
+ registerDoctor(cli);
10789
11427
  registerListRules(cli);
10790
11428
  registerRoundtripTally(cli);
10791
11429
  registerCalibrateAnalyze(cli);
@@ -10815,7 +11453,11 @@ cli.help((sections) => {
10815
11453
  sections.push(
10816
11454
  {
10817
11455
  title: "\nSetup",
10818
- body: ` canicode init --token <token> Save Figma token to ~/.canicode/`
11456
+ body: [
11457
+ ` canicode init Interactive setup (prompts for token)`,
11458
+ ` canicode init --token <token> Non-interactive setup (CI / non-TTY)`,
11459
+ ` canicode config set-token Rotate token without reinstalling skills`
11460
+ ].join("\n")
10819
11461
  },
10820
11462
  {
10821
11463
  title: "\nData source",