canicode 0.11.5 → 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/.claude-plugin/plugin.json +1 -1
- package/README.md +32 -15
- package/dist/cli/index.js +385 -90
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +89 -22
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +102 -29
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +10 -9
- package/package.json +1 -1
- package/skills/canicode-roundtrip/SKILL.md +104 -4
- package/skills/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/canicode-roundtrip/helpers-installer.js +1 -1
- package/skills/cursor/canicode-roundtrip/SKILL.md +104 -4
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +1 -1
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +1 -1
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
|
|
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
|
|
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
|
|
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 --
|
|
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:
|
|
1898
|
-
//
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
severity: "
|
|
1902
|
-
score:
|
|
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: "
|
|
1956
|
-
|
|
1957
|
-
|
|
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: "
|
|
1962
|
-
|
|
1963
|
-
|
|
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.
|
|
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
|
|
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>", "
|
|
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
|
-
|
|
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
|
|
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>", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -7331,27 +7508,7 @@ ${msg}`);
|
|
|
7331
7508
|
}
|
|
7332
7509
|
const options = parseResult.data;
|
|
7333
7510
|
if (options.token) {
|
|
7334
|
-
|
|
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
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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",
|