canicode 0.11.4 → 0.12.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
@@ -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
 
@@ -1371,6 +1372,8 @@ var EVENTS = {
1371
1372
  // CLI
1372
1373
  CLI_COMMAND: `${EVENT_PREFIX}cli_command`,
1373
1374
  CLI_INIT: `${EVENT_PREFIX}cli_init`,
1375
+ CLI_CONFIG_SET_TOKEN: `${EVENT_PREFIX}cli_config_set_token`,
1376
+ CLI_DOCTOR: `${EVENT_PREFIX}cli_doctor`,
1374
1377
  // Roundtrip (ADR-012)
1375
1378
  // Wiring point for the roundtrip helper's `telemetry` callback. No Node-side
1376
1379
  // orchestrator reads this yet — the helper ships in a sandbox-pure IIFE that
@@ -1547,12 +1550,14 @@ CANICODE SETUP GUIDE
1547
1550
  npm install -g canicode
1548
1551
 
1549
1552
  Setup:
1550
- canicode init --token figd_xxxxxxxxxxxxx
1553
+ canicode init (interactive prompt; TTY)
1554
+ canicode init --token figd_xxxxxxxxxxxxx (non-TTY / CI)
1555
+ FIGMA_TOKEN=figd_xxx canicode init (env-driven)
1551
1556
  (saves token + installs Claude Code skills into ./.claude/skills/)
1552
1557
 
1553
1558
  Skills only (no token yet):
1554
1559
  canicode init --cursor-skills
1555
- (installs Claude skills + Cursor copies; run init --token \u2026 before live Figma REST URLs)
1560
+ (installs Claude skills + Cursor copies; run init or init --token \u2026 before live Figma REST URLs)
1556
1561
 
1557
1562
  Use:
1558
1563
  canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
@@ -1562,7 +1567,7 @@ CANICODE SETUP GUIDE
1562
1567
  --preset strict|relaxed|dev-friendly|ai-ready
1563
1568
  --config ./my-config.json
1564
1569
  --no-open Don't open report in browser
1565
- --api No-op for Figma URLs (REST always); same flag as gotcha-survey (#461)
1570
+ --api No-op for Figma URLs (REST always); same flag as gotcha-survey
1566
1571
 
1567
1572
  Output:
1568
1573
  ~/.canicode/reports/report-YYYY-MM-DD-HH-mm-<filekey>.html
@@ -1574,7 +1579,8 @@ CANICODE SETUP GUIDE
1574
1579
  (Same token safety as above \u2014 env var or interactive prompt, not chat.)
1575
1580
 
1576
1581
  Setup:
1577
- canicode init --token figd_xxxxxxxxxxxxx
1582
+ canicode init (interactive prompt; TTY)
1583
+ canicode init --token figd_xxxxxxxxxxxxx (non-TTY / CI)
1578
1584
  (installs three skills into ./.claude/skills/ alongside the token)
1579
1585
 
1580
1586
  Installed skills:
@@ -1599,7 +1605,8 @@ CANICODE SETUP GUIDE
1599
1605
  (Same token safety as above \u2014 env var or interactive prompt, not chat.)
1600
1606
 
1601
1607
  Setup:
1602
- canicode init --token figd_xxxxxxxxxxxxx --cursor-skills
1608
+ canicode init --cursor-skills (interactive prompt; TTY)
1609
+ canicode init --token figd_xxxxxxxxxxxxx --cursor-skills (non-TTY / CI)
1603
1610
  (installs Cursor copies of the three skills into ./.cursor/skills/)
1604
1611
 
1605
1612
  Installed skills:
@@ -1629,6 +1636,14 @@ CANICODE SETUP GUIDE
1629
1636
  See also: docs/CUSTOMIZATION.md#cursor-mcp-canicode (Figma MCP required for roundtrip
1630
1637
  writes; analyze-only works without it).
1631
1638
 
1639
+ \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
1640
+ MANAGE CONFIG
1641
+ \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
1642
+
1643
+ canicode config set-token Rotate Figma token (no skill reinstall)
1644
+ canicode config show Print masked token + config + reports paths
1645
+ canicode config path Print absolute config path (script-friendly)
1646
+
1632
1647
  \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
1648
  TOKEN PRIORITY
1634
1649
  \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 +1836,7 @@ var RULE_ID_CATEGORY = {
1821
1836
  "detached-instance": "code-quality",
1822
1837
  "variant-structure-mismatch": "code-quality",
1823
1838
  "deep-nesting": "code-quality",
1839
+ "unmapped-component": "code-quality",
1824
1840
  // Token Management
1825
1841
  "raw-value": "token-management",
1826
1842
  "irregular-spacing": "token-management",
@@ -1851,6 +1867,12 @@ var RULE_PURPOSE = {
1851
1867
  "detached-instance": "violation",
1852
1868
  "variant-structure-mismatch": "violation",
1853
1869
  "deep-nesting": "violation",
1870
+ // #520: unmapped-component is annotation-primary. Fires only when the
1871
+ // user has Code Connect set up at all (figma.config.json present in cwd).
1872
+ // The gotcha drives the user to /canicode-roundtrip for actual mapping
1873
+ // registration via the Figma MCP tools — analyze itself does not parse
1874
+ // mapping declarations (deferred to v1.5).
1875
+ "unmapped-component": "info-collection",
1854
1876
  // Token Management
1855
1877
  "raw-value": "violation",
1856
1878
  "irregular-spacing": "violation",
@@ -1894,12 +1916,12 @@ var RULE_CONFIGS = {
1894
1916
  enabled: true
1895
1917
  },
1896
1918
  "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,
1919
+ // #403 → #519: info-collection rule. Score is 0 (severity `note`):
1920
+ // its value is the gotcha annotation, not the grade impact. Survey-
1921
+ // generator includes this rule via the `purpose === "info-collection"`
1922
+ // branch so the gotcha keeps surfacing.
1923
+ severity: "note",
1924
+ score: 0,
1903
1925
  enabled: true
1904
1926
  },
1905
1927
  // ── Code Quality ──
@@ -1931,6 +1953,16 @@ var RULE_CONFIGS = {
1931
1953
  maxDepth: 5
1932
1954
  }
1933
1955
  },
1956
+ "unmapped-component": {
1957
+ // #520 / #519: zero-impact tier. Fires per main component when Code
1958
+ // Connect is set up in the consuming repo (figma.config.json at cwd).
1959
+ // Score is 0 because the rule's value is the gotcha + roundtrip handoff,
1960
+ // not the grade signal — designers who deliberately do not map (e.g.
1961
+ // marketing-only banners) are not punished.
1962
+ severity: "note",
1963
+ score: 0,
1964
+ enabled: true
1965
+ },
1934
1966
  // ── Token Management ──
1935
1967
  "raw-value": {
1936
1968
  severity: "missing-info",
@@ -1952,15 +1984,15 @@ var RULE_CONFIGS = {
1952
1984
  // is minimal. Score stays at -1 so re-enabling `missing-prototype` on
1953
1985
  // fixtures that lack `interactionDestinations` (#139) cannot swing grades.
1954
1986
  "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
1987
+ severity: "note",
1988
+ // #519: info-collection rule, zero-score tier
1989
+ score: 0,
1958
1990
  enabled: true
1959
1991
  },
1960
1992
  "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
1993
+ severity: "note",
1994
+ // #519: info-collection — annotation is primary output, no grade impact
1995
+ score: 0,
1964
1996
  enabled: true
1965
1997
  },
1966
1998
  // ── Semantic ──
@@ -2165,7 +2197,14 @@ var SeveritySchema = z.enum([
2165
2197
  "blocking",
2166
2198
  "risk",
2167
2199
  "missing-info",
2168
- "suggestion"
2200
+ "suggestion",
2201
+ /**
2202
+ * `note` is the zero-impact tier (#519): findings render in the report but
2203
+ * never move the grade. Used for annotation-primary rules whose value is the
2204
+ * nudge, not the score (e.g. unmapped Code Connect components, info-collection
2205
+ * rules whose answers belong in figma-implement-design context, not in linting).
2206
+ */
2207
+ "note"
2169
2208
  ]);
2170
2209
 
2171
2210
  // src/core/contracts/rule.ts
@@ -2459,6 +2498,10 @@ var missingComponentMsg = {
2459
2498
  suggestion: `Create a new variant for this style combination`
2460
2499
  })
2461
2500
  };
2501
+ var unmappedComponentMsg = (componentName) => ({
2502
+ message: `"${componentName}" has no Code Connect mapping`,
2503
+ 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.`
2504
+ });
2462
2505
  var detachedInstanceMsg = (name, componentName) => ({
2463
2506
  message: `"${name}" may be a detached instance of component "${componentName}"`,
2464
2507
  suggestion: `Restore as an instance of "${componentName}" or create a new variant`
@@ -3010,8 +3053,6 @@ defineRule({
3010
3053
  definition: irregularSpacingDef,
3011
3054
  check: irregularSpacingCheck
3012
3055
  });
3013
-
3014
- // src/core/rules/component/index.ts
3015
3056
  var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
3016
3057
  function detectStyleOverrides(master, instance) {
3017
3058
  const overrides = [];
@@ -3219,6 +3260,35 @@ defineRule({
3219
3260
  definition: variantStructureMismatchDef,
3220
3261
  check: variantStructureMismatchCheck
3221
3262
  });
3263
+ var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
3264
+ function codeConnectIsSetUp(context) {
3265
+ return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3266
+ return existsSync(join(process.cwd(), "figma.config.json"));
3267
+ });
3268
+ }
3269
+ var unmappedComponentDef = {
3270
+ id: "unmapped-component",
3271
+ name: "Unmapped Component",
3272
+ category: "code-quality",
3273
+ 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.",
3274
+ impact: "Future roundtrips on screens containing this component cannot reuse your existing code; they regenerate markup that may not match the canonical implementation.",
3275
+ fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
3276
+ };
3277
+ var unmappedComponentCheck = (node, context) => {
3278
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3279
+ if (isInsideInstance(context)) return null;
3280
+ if (!codeConnectIsSetUp(context)) return null;
3281
+ return {
3282
+ ruleId: unmappedComponentDef.id,
3283
+ nodeId: node.id,
3284
+ nodePath: context.path.join(" > "),
3285
+ ...unmappedComponentMsg(node.name)
3286
+ };
3287
+ };
3288
+ defineRule({
3289
+ definition: unmappedComponentDef,
3290
+ check: unmappedComponentCheck
3291
+ });
3222
3292
 
3223
3293
  // src/core/rules/naming/index.ts
3224
3294
  function capitalize(s) {
@@ -4187,6 +4257,7 @@ var STRATEGY_BY_RULE = {
4187
4257
  // Strategy C — annotation only
4188
4258
  "absolute-position-in-auto-layout": "annotation",
4189
4259
  "variant-structure-mismatch": "annotation",
4260
+ "unmapped-component": "annotation",
4190
4261
  // Strategy D — auto-fix lower-severity issues from analyze output
4191
4262
  "non-standard-naming": "auto-fix",
4192
4263
  "inconsistent-naming-convention": "auto-fix",
@@ -4221,6 +4292,7 @@ function resolveTargetProperty(ruleId, subType) {
4221
4292
  case "raw-value":
4222
4293
  case "missing-interaction-state":
4223
4294
  case "missing-prototype":
4295
+ case "unmapped-component":
4224
4296
  return void 0;
4225
4297
  }
4226
4298
  }
@@ -4245,7 +4317,7 @@ function computeApplyContext(violation, instanceContext) {
4245
4317
  }
4246
4318
 
4247
4319
  // package.json
4248
- var version2 = "0.11.4";
4320
+ var version2 = "0.12.0";
4249
4321
 
4250
4322
  // src/core/engine/scoring.ts
4251
4323
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -4352,6 +4424,7 @@ function calculateScores(result, configs) {
4352
4424
  risk: 0,
4353
4425
  missingInfo: 0,
4354
4426
  suggestion: 0,
4427
+ note: 0,
4355
4428
  nodeCount,
4356
4429
  acknowledgedCount: 0
4357
4430
  };
@@ -4369,6 +4442,9 @@ function calculateScores(result, configs) {
4369
4442
  case "suggestion":
4370
4443
  summary.suggestion++;
4371
4444
  break;
4445
+ case "note":
4446
+ summary.note++;
4447
+ break;
4372
4448
  }
4373
4449
  if (issue.acknowledged === true) summary.acknowledgedCount++;
4374
4450
  }
@@ -4400,7 +4476,8 @@ function initializeCategoryScores() {
4400
4476
  blocking: 0,
4401
4477
  risk: 0,
4402
4478
  "missing-info": 0,
4403
- suggestion: 0
4479
+ suggestion: 0,
4480
+ note: 0
4404
4481
  }
4405
4482
  };
4406
4483
  }
@@ -4421,6 +4498,7 @@ function formatScoreSummary(report) {
4421
4498
  lines.push(` Risk: ${report.summary.risk}`);
4422
4499
  lines.push(` Missing Info: ${report.summary.missingInfo}`);
4423
4500
  lines.push(` Suggestion: ${report.summary.suggestion}`);
4501
+ lines.push(` Note: ${report.summary.note}`);
4424
4502
  if (report.summary.acknowledgedCount > 0) {
4425
4503
  const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
4426
4504
  lines.push(
@@ -4571,7 +4649,8 @@ function severityDot(sev) {
4571
4649
  blocking: "sev-blocking",
4572
4650
  risk: "sev-risk",
4573
4651
  "missing-info": "sev-missing",
4574
- suggestion: "sev-suggestion"
4652
+ suggestion: "sev-suggestion",
4653
+ note: "sev-note"
4575
4654
  };
4576
4655
  return map[sev];
4577
4656
  }
@@ -4580,7 +4659,8 @@ function severityBadge(sev) {
4580
4659
  blocking: "sev-blocking",
4581
4660
  risk: "sev-risk",
4582
4661
  "missing-info": "sev-missing",
4583
- suggestion: "sev-suggestion"
4662
+ suggestion: "sev-suggestion",
4663
+ note: "sev-note"
4584
4664
  };
4585
4665
  return map[sev];
4586
4666
  }
@@ -4638,6 +4718,7 @@ ${CATEGORIES.map((cat) => {
4638
4718
  ${renderSummaryDot("sev-risk", scores.summary.risk, "Risk")}
4639
4719
  ${renderSummaryDot("sev-missing", scores.summary.missingInfo, "Missing Info")}
4640
4720
  ${renderSummaryDot("sev-suggestion", scores.summary.suggestion, "Suggestion")}
4721
+ ${renderSummaryDot("sev-note", scores.summary.note, "Note")}
4641
4722
  <div class="rpt-summary-total">
4642
4723
  <span class="rpt-summary-count">${scores.summary.totalIssues}</span>
4643
4724
  <span class="rpt-summary-label">Total</span>
@@ -4852,7 +4933,7 @@ function groupIssuesByRule(issues) {
4852
4933
  group.issues.push(issue);
4853
4934
  group.totalScore += issue.calculatedScore;
4854
4935
  }
4855
- const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3 };
4936
+ const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3, note: 4 };
4856
4937
  return [...byRule.values()].sort((a, b) => {
4857
4938
  const sevDiff = (SEVERITY_RANK[a.severity] ?? 4) - (SEVERITY_RANK[b.severity] ?? 4);
4858
4939
  return sevDiff !== 0 ? sevDiff : a.totalScore - b.totalScore;
@@ -4920,6 +5001,7 @@ body {
4920
5001
  .sev-risk { background: var(--amber); }
4921
5002
  .sev-missing { background: #a1a1aa; }
4922
5003
  .sev-suggestion { background: var(--green); }
5004
+ .sev-note { background: #d4d4d8; }
4923
5005
 
4924
5006
  /* ---- Print ---- */
4925
5007
  @media print {
@@ -5316,6 +5398,7 @@ body {
5316
5398
  .rpt-issue-score.sev-risk { background: var(--amber-bg); color: #d97706; border-color: rgba(245,158,11,0.2); }
5317
5399
  .rpt-issue-score.sev-missing { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
5318
5400
  .rpt-issue-score.sev-suggestion { background: var(--green-bg); color: #16a34a; border-color: rgba(34,197,94,0.2); }
5401
+ .rpt-issue-score.sev-note { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
5319
5402
 
5320
5403
  .rpt-issue-body {
5321
5404
  padding: 12px;
@@ -5741,8 +5824,8 @@ var AnalyzeOptionsSchema = z.object({
5741
5824
  function registerAnalyze(cli2) {
5742
5825
  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
5826
  "--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) => {
5827
+ "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `gotcha-survey`."
5828
+ ).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
5829
  const parseResult = AnalyzeOptionsSchema.safeParse(rawOptions);
5747
5830
  if (!parseResult.success) {
5748
5831
  const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
@@ -5961,6 +6044,12 @@ var GOTCHA_QUESTION_CONTENT = {
5961
6044
  hint: "Describe which variant has the correct structure, or if they should all match",
5962
6045
  example: "Default variant is canonical \u2014 other variants should toggle child visibility instead of adding/removing elements"
5963
6046
  },
6047
+ "unmapped-component": {
6048
+ ruleId: "unmapped-component",
6049
+ question: '"{nodeName}" has no Code Connect mapping yet. Should we register one so figma-implement-design reuses your code?',
6050
+ hint: "Skip if this component is intentionally unmapped (e.g. marketing-only banner). Otherwise run /canicode-roundtrip on the component to walk through registration.",
6051
+ example: "Yes \u2014 map to src/components/Button.tsx so future screens reuse the existing implementation"
6052
+ },
5964
6053
  "deep-nesting": {
5965
6054
  ruleId: "deep-nesting",
5966
6055
  question: '"{nodeName}" is deeply nested. Can some intermediate layers be flattened or extracted?',
@@ -6106,10 +6195,7 @@ function generateGotchaSurvey(result, scores, options = {}) {
6106
6195
  const relevantIssues = result.issues.filter((issue) => {
6107
6196
  const severity = issue.config.severity;
6108
6197
  if (severity === "blocking" || severity === "risk") return true;
6109
- if (severity === "missing-info") {
6110
- return getRulePurpose(issue.violation.ruleId) === "info-collection";
6111
- }
6112
- return false;
6198
+ return getRulePurpose(issue.violation.ruleId) === "info-collection";
6113
6199
  });
6114
6200
  const deduped = deduplicateSiblingIssues(relevantIssues);
6115
6201
  const sorted = stableSortBySeverity(deduped);
@@ -6322,8 +6408,8 @@ function formatHumanSummary(survey) {
6322
6408
  function registerGotchaSurvey(cli2) {
6323
6409
  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
6410
  "--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) => {
6411
+ "No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `analyze`."
6412
+ ).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
6413
  const parseResult = GotchaSurveyOptionsSchema.safeParse(rawOptions);
6328
6414
  if (!parseResult.success) {
6329
6415
  const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
@@ -6748,7 +6834,7 @@ var UpsertOptionsSchema = z.object({
6748
6834
  });
6749
6835
  var USER_MESSAGES = {
6750
6836
  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."
6837
+ 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
6838
  };
6753
6839
  async function readStdin() {
6754
6840
  const chunks = [];
@@ -7189,6 +7275,47 @@ async function promptOverwriteBatch(candidates) {
7189
7275
  }
7190
7276
  return decisions;
7191
7277
  }
7278
+ var NonInteractiveError = class extends Error {
7279
+ constructor(message = "Interactive prompt requires a TTY") {
7280
+ super(message);
7281
+ this.name = "NonInteractiveError";
7282
+ }
7283
+ };
7284
+ async function promptForFigmaToken(opts = {}) {
7285
+ const isTTY = opts.isTTY ?? process.stdin.isTTY ?? false;
7286
+ if (!isTTY) {
7287
+ throw new NonInteractiveError();
7288
+ }
7289
+ const input = opts.input ?? process.stdin;
7290
+ const output = opts.output ?? process.stdout;
7291
+ const maxAttempts = opts.maxAttempts ?? 3;
7292
+ const rl = readline.createInterface({ input, output });
7293
+ try {
7294
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
7295
+ const answer = (await rl.question("Figma token: ")).trim();
7296
+ if (answer.length > 0) {
7297
+ return answer;
7298
+ }
7299
+ if (attempt < maxAttempts) {
7300
+ output.write("Token cannot be empty. Try again.\n");
7301
+ }
7302
+ }
7303
+ throw new Error(`No token provided after ${maxAttempts} attempts`);
7304
+ } finally {
7305
+ rl.close();
7306
+ }
7307
+ }
7308
+ function maskFigmaToken(token) {
7309
+ if (!token) return "(empty)";
7310
+ const BULLETS = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
7311
+ if (token.startsWith("figd_") && token.length > 9) {
7312
+ return `figd_${BULLETS}${token.slice(-4)}`;
7313
+ }
7314
+ if (token.length >= 4) {
7315
+ return `${BULLETS}${token.slice(-4)}`;
7316
+ }
7317
+ return "\u2022".repeat(token.length);
7318
+ }
7192
7319
 
7193
7320
  // src/cli/commands/init.ts
7194
7321
  function figmaEntryInMcpFile(filePath) {
@@ -7217,7 +7344,7 @@ function formatNextSteps(opts) {
7217
7344
  " Next:",
7218
7345
  " 1. Restart Cursor or reload MCP (so skills + MCP tools load in a fresh session)",
7219
7346
  " 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)."
7347
+ " \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
7348
  ].join("\n");
7222
7349
  }
7223
7350
  return [
@@ -7225,7 +7352,7 @@ function formatNextSteps(opts) {
7225
7352
  " Next:",
7226
7353
  " 1. Restart Claude Code (the newly installed skills only load on a fresh session)",
7227
7354
  " 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)."
7355
+ " \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
7356
  ].join("\n");
7230
7357
  }
7231
7358
  if (cursor) {
@@ -7235,7 +7362,7 @@ function formatNextSteps(opts) {
7235
7362
  " 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
7363
  " 2. Restart Cursor so Figma tools (e.g. use_figma) load",
7237
7364
  " 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)."
7365
+ " \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
7366
  ].join("\n");
7240
7367
  }
7241
7368
  return [
@@ -7245,7 +7372,7 @@ function formatNextSteps(opts) {
7245
7372
  " claude mcp add -s project -t http figma https://mcp.figma.com/mcp",
7246
7373
  " 2. Restart Claude Code (so the new skills + Figma MCP tools both load)",
7247
7374
  " 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)."
7375
+ " \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
7376
  ].join("\n");
7250
7377
  }
7251
7378
  var InitOptionsSchema = z.object({
@@ -7258,6 +7385,56 @@ var InitOptionsSchema = z.object({
7258
7385
  function wantsSkillInstallWithoutToken(options) {
7259
7386
  return options.cursorSkills === true;
7260
7387
  }
7388
+ async function runFullInit(token, options, interactive) {
7389
+ initAiready(token);
7390
+ console.log(` Config saved: ${getConfigPath()}`);
7391
+ console.log(` Reports will be saved to: ${getReportsDir()}/`);
7392
+ const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
7393
+ trackEvent(EVENTS.CLI_INIT, {
7394
+ skillsRequested: true,
7395
+ cursorSkillsRequested: options.cursorSkills === true,
7396
+ skillStepOk,
7397
+ target: options.global ? "global" : "project",
7398
+ force: options.force ?? false,
7399
+ interactive,
7400
+ ...skillSummary ?? {}
7401
+ });
7402
+ if (skillStepOk) {
7403
+ console.log(
7404
+ formatNextSteps({
7405
+ figmaMcpPresent: figmaMcpRegistered(),
7406
+ skillsInstalled: true,
7407
+ cursorSkillsInstalled: options.cursorSkills === true
7408
+ })
7409
+ );
7410
+ }
7411
+ }
7412
+ function printSetupGuide() {
7413
+ console.log(`CANICODE SETUP
7414
+ `);
7415
+ console.log(
7416
+ ` Never paste your token into Claude/Cursor chat \u2014 use FIGMA_TOKEN=\u2026 npx canicode init or this prompt only.
7417
+ `
7418
+ );
7419
+ console.log(` canicode init (interactive prompt; TTY only)`);
7420
+ console.log(` canicode init --token YOUR_FIGMA_TOKEN (CI / non-TTY)`);
7421
+ console.log(` Get token: figma.com > Settings > Personal access tokens
7422
+ `);
7423
+ console.log(`Skills:`);
7424
+ console.log(` --token (or the interactive prompt) also installs three Claude Code skills`);
7425
+ console.log(` into ./.claude/skills/ (canicode, canicode-gotchas, canicode-roundtrip).`);
7426
+ console.log(` --global Install to ~/.claude/skills/ instead`);
7427
+ 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)`);
7428
+ console.log(` --force Overwrite existing skill files without prompting
7429
+ `);
7430
+ console.log(`Manage saved config (no skill reinstall):`);
7431
+ console.log(` canicode config set-token Rotate saved Figma token`);
7432
+ console.log(` canicode config show Print masked token + paths`);
7433
+ console.log(` canicode config path Print absolute config path
7434
+ `);
7435
+ console.log(`After setup:`);
7436
+ console.log(` canicode analyze "https://www.figma.com/design/..."`);
7437
+ }
7261
7438
  async function runInitSkillInstallSteps(options) {
7262
7439
  let skillStepOk = true;
7263
7440
  let skillSummary;
@@ -7268,9 +7445,9 @@ async function runInitSkillInstallSteps(options) {
7268
7445
  });
7269
7446
  console.log(`
7270
7447
  Skills installed to: ${summary.targetDir}/`);
7271
- console.log(` installed: ${summary.installed.length}`);
7272
- console.log(` overwritten: ${summary.overwritten.length}`);
7273
- console.log(` skipped: ${summary.skipped.length}`);
7448
+ console.log(` files installed: ${summary.installed.length}`);
7449
+ console.log(` files overwritten: ${summary.overwritten.length}`);
7450
+ console.log(` files skipped: ${summary.skipped.length}`);
7274
7451
  if (summary.skipped.length > 0) {
7275
7452
  console.log(` (Re-run with --force to overwrite skipped files.)`);
7276
7453
  }
@@ -7294,9 +7471,9 @@ async function runInitSkillInstallSteps(options) {
7294
7471
  });
7295
7472
  console.log(`
7296
7473
  Cursor skills installed to: ${cSummary.targetDir}/`);
7297
- console.log(` installed: ${cSummary.installed.length}`);
7298
- console.log(` overwritten: ${cSummary.overwritten.length}`);
7299
- console.log(` skipped: ${cSummary.skipped.length}`);
7474
+ console.log(` files installed: ${cSummary.installed.length}`);
7475
+ console.log(` files overwritten: ${cSummary.overwritten.length}`);
7476
+ console.log(` files skipped: ${cSummary.skipped.length}`);
7300
7477
  if (cSummary.skipped.length > 0) {
7301
7478
  console.log(` (Re-run with --force to overwrite skipped files.)`);
7302
7479
  }
@@ -7331,27 +7508,7 @@ ${msg}`);
7331
7508
  }
7332
7509
  const options = parseResult.data;
7333
7510
  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
- }
7511
+ await runFullInit(options.token, options, false);
7355
7512
  return;
7356
7513
  }
7357
7514
  if (wantsSkillInstallWithoutToken(options)) {
@@ -7379,24 +7536,16 @@ ${msg}`);
7379
7536
  }
7380
7537
  return;
7381
7538
  }
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/..."`);
7539
+ try {
7540
+ const token = await promptForFigmaToken();
7541
+ await runFullInit(token, options, true);
7542
+ return;
7543
+ } catch (promptError) {
7544
+ if (!(promptError instanceof NonInteractiveError)) {
7545
+ throw promptError;
7546
+ }
7547
+ }
7548
+ printSetupGuide();
7400
7549
  } catch (error) {
7401
7550
  console.error(
7402
7551
  "\nError:",
@@ -7408,28 +7557,85 @@ ${msg}`);
7408
7557
  }
7409
7558
 
7410
7559
  // src/cli/commands/config.ts
7560
+ var VALID_ACTIONS = ["set-token", "show", "path"];
7561
+ function isConfigAction(value) {
7562
+ return value !== void 0 && VALID_ACTIONS.includes(value);
7563
+ }
7564
+ function printConfigShow() {
7565
+ const cfg = readConfig();
7566
+ const envToken = process.env["FIGMA_TOKEN"];
7567
+ const effectiveToken = getFigmaToken();
7568
+ const tokenSource = envToken ? " (env: FIGMA_TOKEN)" : "";
7569
+ console.log("CANICODE CONFIG\n");
7570
+ console.log(` Config path: ${getConfigPath()}`);
7571
+ console.log(` Reports dir: ${getReportsDir()}`);
7572
+ console.log(` Figma token: ${maskFigmaToken(effectiveToken)}${tokenSource}`);
7573
+ console.log(` Telemetry: ${cfg.telemetry !== false ? "enabled" : "disabled"}`);
7574
+ console.log(`
7575
+ Options:`);
7576
+ console.log(` canicode config set-token Update saved Figma token (no skill reinstall)`);
7577
+ console.log(` canicode config show Show current configuration`);
7578
+ console.log(` canicode config path Print absolute config path`);
7579
+ console.log(` canicode config --no-telemetry Opt out of anonymous telemetry`);
7580
+ console.log(` canicode config --telemetry Opt back in`);
7581
+ }
7582
+ async function handleSetToken(options) {
7583
+ let token = options.token;
7584
+ const usedFlag = Boolean(token);
7585
+ if (!token) {
7586
+ try {
7587
+ token = await promptForFigmaToken();
7588
+ } catch (err) {
7589
+ if (err instanceof NonInteractiveError) {
7590
+ console.error(
7591
+ "Run with --token <token> or set FIGMA_TOKEN=\u2026 (interactive prompt requires a TTY)."
7592
+ );
7593
+ process.exitCode = 1;
7594
+ return;
7595
+ }
7596
+ throw err;
7597
+ }
7598
+ }
7599
+ setFigmaToken(token);
7600
+ console.log(`Token saved: ${getConfigPath()}`);
7601
+ trackEvent(EVENTS.CLI_CONFIG_SET_TOKEN, { interactive: !usedFlag });
7602
+ }
7411
7603
  function registerConfig(cli2) {
7412
- cli2.command("config", "Manage canicode configuration").option("--telemetry", "Enable anonymous telemetry").option("--no-telemetry", "Disable anonymous telemetry").action((options) => {
7604
+ 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
7605
  try {
7414
- if (options.telemetry === false) {
7606
+ if (action !== void 0 && !isConfigAction(action)) {
7607
+ console.error(
7608
+ `Unknown config action: ${action}. Available: ${VALID_ACTIONS.join(", ")}`
7609
+ );
7610
+ process.exitCode = 1;
7611
+ return;
7612
+ }
7613
+ if (action === "set-token") {
7614
+ await handleSetToken(options);
7615
+ return;
7616
+ }
7617
+ if (action === "path") {
7618
+ console.log(getConfigPath());
7619
+ return;
7620
+ }
7621
+ if (action === "show") {
7622
+ printConfigShow();
7623
+ return;
7624
+ }
7625
+ const argv = process.argv.slice(2);
7626
+ const flippedOff = argv.includes("--no-telemetry");
7627
+ const flippedOn = argv.includes("--telemetry");
7628
+ if (flippedOff) {
7415
7629
  setTelemetryEnabled(false);
7416
7630
  console.log("Telemetry disabled. No analytics data will be sent.");
7417
7631
  return;
7418
7632
  }
7419
- if (options.telemetry === true) {
7633
+ if (flippedOn) {
7420
7634
  setTelemetryEnabled(true);
7421
7635
  console.log("Telemetry enabled. Only anonymous usage events are tracked \u2014 no design data.");
7422
7636
  return;
7423
7637
  }
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`);
7638
+ printConfigShow();
7433
7639
  } catch (error) {
7434
7640
  console.error(
7435
7641
  "\nError:",
@@ -7439,6 +7645,90 @@ Options:`);
7439
7645
  }
7440
7646
  });
7441
7647
  }
7648
+ var CODE_CONNECT_PKG = "@figma/code-connect";
7649
+ var CODE_CONNECT_DOCS = "https://www.figma.com/code-connect-docs/";
7650
+ function readPackageJson(cwd) {
7651
+ const pkgPath = join(cwd, "package.json");
7652
+ if (!existsSync(pkgPath)) return void 0;
7653
+ try {
7654
+ return JSON.parse(readFileSync(pkgPath, "utf-8"));
7655
+ } catch {
7656
+ return void 0;
7657
+ }
7658
+ }
7659
+ function findCodeConnectVersion(pkg3) {
7660
+ if (!pkg3) return void 0;
7661
+ return pkg3.dependencies?.[CODE_CONNECT_PKG] ?? pkg3.devDependencies?.[CODE_CONNECT_PKG];
7662
+ }
7663
+ function runCodeConnectChecks(cwd) {
7664
+ const pkg3 = readPackageJson(cwd);
7665
+ const ccVersion = findCodeConnectVersion(pkg3);
7666
+ const figmaConfigExists = existsSync(join(cwd, "figma.config.json"));
7667
+ const results = [];
7668
+ if (ccVersion) {
7669
+ results.push({
7670
+ name: `${CODE_CONNECT_PKG} installed`,
7671
+ pass: true,
7672
+ detail: ccVersion
7673
+ });
7674
+ } else {
7675
+ results.push({
7676
+ name: `${CODE_CONNECT_PKG} not installed`,
7677
+ pass: false,
7678
+ 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.`
7679
+ });
7680
+ }
7681
+ if (figmaConfigExists) {
7682
+ results.push({
7683
+ name: "figma.config.json found at repo root",
7684
+ pass: true
7685
+ });
7686
+ } else {
7687
+ results.push({
7688
+ name: "figma.config.json not found at repo root",
7689
+ pass: false,
7690
+ remediation: `see ${CODE_CONNECT_DOCS}`
7691
+ });
7692
+ }
7693
+ return results;
7694
+ }
7695
+ function formatDoctorReport(results) {
7696
+ const lines = ["Code Connect"];
7697
+ for (const result of results) {
7698
+ const icon = result.pass ? "\u2705" : "\u274C";
7699
+ const detail = result.detail ? ` (${result.detail})` : "";
7700
+ lines.push(` ${icon} ${result.name}${detail}`);
7701
+ if (!result.pass && result.remediation) {
7702
+ lines.push(` \u2192 ${result.remediation}`);
7703
+ }
7704
+ }
7705
+ lines.push("");
7706
+ const allPass = results.every((r) => r.pass);
7707
+ lines.push(
7708
+ allPass ? "All checks passed." : "Some checks failed. Fix the items above before running the Code Connect flow."
7709
+ );
7710
+ return lines.join("\n");
7711
+ }
7712
+ function registerDoctor(cli2) {
7713
+ cli2.command(
7714
+ "doctor",
7715
+ "Diagnose Code Connect prerequisites (`@figma/code-connect`, `figma.config.json`)"
7716
+ ).action(() => {
7717
+ const cwd = process.cwd();
7718
+ const results = runCodeConnectChecks(cwd);
7719
+ console.log(formatDoctorReport(results));
7720
+ const passed = results.filter((r) => r.pass).length;
7721
+ const failed = results.length - passed;
7722
+ trackEvent(EVENTS.CLI_DOCTOR, {
7723
+ passed,
7724
+ failed,
7725
+ total: results.length
7726
+ });
7727
+ if (failed > 0) {
7728
+ process.exitCode = 1;
7729
+ }
7730
+ });
7731
+ }
7442
7732
 
7443
7733
  // src/cli/commands/list-rules.ts
7444
7734
  function registerListRules(cli2) {
@@ -10786,6 +11076,7 @@ registerDesignTree(cli);
10786
11076
  registerVisualCompare(cli);
10787
11077
  registerInit(cli);
10788
11078
  registerConfig(cli);
11079
+ registerDoctor(cli);
10789
11080
  registerListRules(cli);
10790
11081
  registerRoundtripTally(cli);
10791
11082
  registerCalibrateAnalyze(cli);
@@ -10815,7 +11106,11 @@ cli.help((sections) => {
10815
11106
  sections.push(
10816
11107
  {
10817
11108
  title: "\nSetup",
10818
- body: ` canicode init --token <token> Save Figma token to ~/.canicode/`
11109
+ body: [
11110
+ ` canicode init Interactive setup (prompts for token)`,
11111
+ ` canicode init --token <token> Non-interactive setup (CI / non-TTY)`,
11112
+ ` canicode config set-token Rotate token without reinstalling skills`
11113
+ ].join("\n")
10819
11114
  },
10820
11115
  {
10821
11116
  title: "\nData source",