canicode 0.11.1 → 0.11.3
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/README.md +2 -2
- package/dist/cli/index.js +188 -191
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -9
- package/dist/index.js +33 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +84 -20
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +40 -2
- package/package.json +3 -2
- package/skills/canicode-gotchas/SKILL.md +74 -12
- package/skills/canicode-roundtrip/SKILL.md +69 -10
- package/skills/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/canicode-roundtrip/helpers-installer.js +14 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +74 -12
- package/skills/cursor/canicode-roundtrip/SKILL.md +69 -10
- package/skills/cursor/canicode-roundtrip/canicode-roundtrip-helpers.d.ts +54 -0
- package/skills/cursor/canicode-roundtrip/helpers-bootstrap.js +21 -0
- package/skills/cursor/canicode-roundtrip/helpers-installer.js +14 -0
package/README.md
CHANGED
|
@@ -152,7 +152,7 @@ Drops three skills into `./.claude/skills/`:
|
|
|
152
152
|
|
|
153
153
|
The skills shell out to `npx canicode …` for analyze / gotcha-survey when the canicode MCP server is not installed — both paths produce the same JSON shape. The Figma MCP server is still required for the apply step (Step 4 in `/canicode-roundtrip`); see the prereq note above.
|
|
154
154
|
|
|
155
|
-
Flags: `--global` installs into `~/.claude/skills/` instead. `--
|
|
155
|
+
Flags: `--global` installs into `~/.claude/skills/` instead. `--cursor-skills` also installs Cursor copies under `.cursor/skills/`. `--force` overwrites existing skill files without prompting. Run `canicode docs setup` for the full setup guide.
|
|
156
156
|
|
|
157
157
|
</details>
|
|
158
158
|
|
|
@@ -182,7 +182,7 @@ For Cursor / Claude Desktop config, see [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZA
|
|
|
182
182
|
npx canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
Setup: `npx canicode init --token figd_xxxxxxxxxxxxx` saves the token
|
|
185
|
+
Setup: `npx canicode init --token figd_xxxxxxxxxxxxx` saves the token and installs the Claude Code skills into `./.claude/skills/`.
|
|
186
186
|
|
|
187
187
|
**Figma API Rate Limits** — Rate limits depend on **where the file lives**, not just your plan.
|
|
188
188
|
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync,
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync, renameSync, chmodSync, copyFileSync } from 'fs';
|
|
3
3
|
import { join, resolve, dirname, basename, relative } from 'path';
|
|
4
4
|
import pixelmatch from 'pixelmatch';
|
|
5
5
|
import { PNG } from 'pngjs';
|
|
@@ -1548,7 +1548,11 @@ CANICODE SETUP GUIDE
|
|
|
1548
1548
|
|
|
1549
1549
|
Setup:
|
|
1550
1550
|
canicode init --token figd_xxxxxxxxxxxxx
|
|
1551
|
-
(saves token + installs
|
|
1551
|
+
(saves token + installs Claude Code skills into ./.claude/skills/)
|
|
1552
|
+
|
|
1553
|
+
Skills only (no token yet):
|
|
1554
|
+
canicode init --cursor-skills
|
|
1555
|
+
(installs Claude skills + Cursor copies; run init --token \u2026 before live Figma REST URLs)
|
|
1552
1556
|
|
|
1553
1557
|
Use:
|
|
1554
1558
|
canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
@@ -1558,6 +1562,7 @@ CANICODE SETUP GUIDE
|
|
|
1558
1562
|
--preset strict|relaxed|dev-friendly|ai-ready
|
|
1559
1563
|
--config ./my-config.json
|
|
1560
1564
|
--no-open Don't open report in browser
|
|
1565
|
+
--api No-op for Figma URLs (REST always); same flag as gotcha-survey (#461)
|
|
1561
1566
|
|
|
1562
1567
|
Output:
|
|
1563
1568
|
~/.canicode/reports/report-YYYY-MM-DD-HH-mm-<filekey>.html
|
|
@@ -1579,7 +1584,7 @@ CANICODE SETUP GUIDE
|
|
|
1579
1584
|
|
|
1580
1585
|
Flags:
|
|
1581
1586
|
--global Install to ~/.claude/skills/ instead of ./.claude/skills/
|
|
1582
|
-
--
|
|
1587
|
+
--cursor-skills Also install Cursor copies (see \xA73)
|
|
1583
1588
|
--force Overwrite existing skill files without prompting
|
|
1584
1589
|
|
|
1585
1590
|
Use (in Claude Code):
|
|
@@ -1604,8 +1609,8 @@ CANICODE SETUP GUIDE
|
|
|
1604
1609
|
|
|
1605
1610
|
Flags:
|
|
1606
1611
|
--cursor-skills Install Cursor copies of all three skills into .cursor/skills/
|
|
1607
|
-
|
|
1608
|
-
|
|
1612
|
+
(with --token, runs after full Claude skill install; without token,
|
|
1613
|
+
installs Claude skills under .claude/skills/ first, then Cursor copies)
|
|
1609
1614
|
--force Overwrite existing skill files without prompting
|
|
1610
1615
|
|
|
1611
1616
|
Use (in Cursor Agent chat):
|
|
@@ -1613,6 +1618,14 @@ CANICODE SETUP GUIDE
|
|
|
1613
1618
|
@canicode-gotchas <figma-url> Run a gotcha survey
|
|
1614
1619
|
@canicode-roundtrip <figma-url> Analyze, fix gotchas in Figma, re-analyze
|
|
1615
1620
|
|
|
1621
|
+
Invocation: docs default to @-skills for Agent chat. If your team uses slash
|
|
1622
|
+
commands or Cursor rules instead, use those \u2014 capability is the same once MCP
|
|
1623
|
+
+ skills load.
|
|
1624
|
+
|
|
1625
|
+
MCP files: Cursor reads .cursor/mcp.json (or ~/.cursor/mcp.json), not the
|
|
1626
|
+
repo-root .mcp.json used by Claude Code \u2014 duplicate Figma + canicode entries
|
|
1627
|
+
if you use both hosts (see docs/CUSTOMIZATION.md#cursor-mcp-canicode).
|
|
1628
|
+
|
|
1616
1629
|
See also: docs/CUSTOMIZATION.md#cursor-mcp-canicode (Figma MCP required for roundtrip
|
|
1617
1630
|
writes; analyze-only works without it).
|
|
1618
1631
|
|
|
@@ -4232,9 +4245,11 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4232
4245
|
}
|
|
4233
4246
|
|
|
4234
4247
|
// package.json
|
|
4235
|
-
var version2 = "0.11.
|
|
4248
|
+
var version2 = "0.11.3";
|
|
4236
4249
|
|
|
4237
4250
|
// src/core/engine/scoring.ts
|
|
4251
|
+
var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
|
|
4252
|
+
var DEFAULT_CODEGEN_READY_MIN_GRADE = "A";
|
|
4238
4253
|
function computeTotalScorePerCategory(configs) {
|
|
4239
4254
|
const totals = Object.fromEntries(
|
|
4240
4255
|
CATEGORIES.map((c) => [c, 0])
|
|
@@ -4261,8 +4276,9 @@ function calculateGrade(percentage) {
|
|
|
4261
4276
|
if (percentage >= 50) return "D";
|
|
4262
4277
|
return "F";
|
|
4263
4278
|
}
|
|
4264
|
-
function isReadyForCodeGen(grade) {
|
|
4265
|
-
|
|
4279
|
+
function isReadyForCodeGen(grade, minGrade) {
|
|
4280
|
+
const threshold = minGrade ?? DEFAULT_CODEGEN_READY_MIN_GRADE;
|
|
4281
|
+
return GRADE_ORDER.indexOf(grade) <= GRADE_ORDER.indexOf(threshold);
|
|
4266
4282
|
}
|
|
4267
4283
|
function clamp(value, min, max) {
|
|
4268
4284
|
return Math.max(min, Math.min(max, value));
|
|
@@ -4455,7 +4471,7 @@ function buildResultJson(fileName, result, scores, options) {
|
|
|
4455
4471
|
scope: result.scope,
|
|
4456
4472
|
issueCount: result.issues.length,
|
|
4457
4473
|
acknowledgedCount: scores.summary.acknowledgedCount,
|
|
4458
|
-
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
|
|
4474
|
+
isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade, options?.codegenReadyMinGrade),
|
|
4459
4475
|
blockingIssueCount: scores.summary.blocking,
|
|
4460
4476
|
scores: {
|
|
4461
4477
|
overall: scores.overall,
|
|
@@ -4492,6 +4508,7 @@ var ConfigFileSchema = z.object({
|
|
|
4492
4508
|
excludeNodeTypes: z.array(z.string()).optional(),
|
|
4493
4509
|
excludeNodeNames: z.array(z.string()).optional(),
|
|
4494
4510
|
gridBase: z.number().int().positive().optional(),
|
|
4511
|
+
codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional(),
|
|
4495
4512
|
rules: z.record(z.string(), RuleOverrideSchema).superRefine((rules, ctx) => {
|
|
4496
4513
|
const unknown = Object.keys(rules).filter((id) => !VALID_RULE_IDS.has(id));
|
|
4497
4514
|
if (unknown.length > 0) {
|
|
@@ -5711,16 +5728,21 @@ var AnalyzeOptionsSchema = z.object({
|
|
|
5711
5728
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional(),
|
|
5712
5729
|
output: z.string().optional(),
|
|
5713
5730
|
token: z.string().optional(),
|
|
5731
|
+
/** Accepted for CLI parity; Figma URL loads always use the REST API today (#461). */
|
|
5714
5732
|
api: z.boolean().optional(),
|
|
5715
5733
|
screenshot: z.boolean().optional(),
|
|
5716
5734
|
config: z.string().optional(),
|
|
5717
5735
|
noOpen: z.boolean().optional(),
|
|
5718
5736
|
json: z.boolean().optional(),
|
|
5719
5737
|
acknowledgments: z.string().optional(),
|
|
5720
|
-
scope: z.enum(["page", "component"]).optional()
|
|
5738
|
+
scope: z.enum(["page", "component"]).optional(),
|
|
5739
|
+
readyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional()
|
|
5721
5740
|
});
|
|
5722
5741
|
function registerAnalyze(cli2) {
|
|
5723
|
-
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(
|
|
5742
|
+
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
|
+
"--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) => {
|
|
5724
5746
|
const parseResult = AnalyzeOptionsSchema.safeParse(rawOptions);
|
|
5725
5747
|
if (!parseResult.success) {
|
|
5726
5748
|
const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -5730,6 +5752,7 @@ ${msg}`);
|
|
|
5730
5752
|
process.exit(1);
|
|
5731
5753
|
}
|
|
5732
5754
|
const options = parseResult.data;
|
|
5755
|
+
void options.api;
|
|
5733
5756
|
const analysisStart = Date.now();
|
|
5734
5757
|
trackEvent(EVENTS.ANALYSIS_STARTED, { source: isJsonFile(input) || isFixtureDir(input) ? "fixture" : "figma" });
|
|
5735
5758
|
const log = options.json ? console.error.bind(console) : console.log.bind(console);
|
|
@@ -5782,13 +5805,16 @@ Analyzing: ${file.name}`);
|
|
|
5782
5805
|
let configs = options.preset ? { ...getConfigsWithPreset(options.preset) } : { ...RULE_CONFIGS };
|
|
5783
5806
|
let excludeNodeNames;
|
|
5784
5807
|
let excludeNodeTypes;
|
|
5808
|
+
let codegenReadyMinGrade;
|
|
5785
5809
|
if (options.config) {
|
|
5786
5810
|
const configFile = await loadConfigFile(options.config);
|
|
5787
5811
|
configs = mergeConfigs(configs, configFile);
|
|
5788
5812
|
excludeNodeNames = configFile.excludeNodeNames;
|
|
5789
5813
|
excludeNodeTypes = configFile.excludeNodeTypes;
|
|
5814
|
+
codegenReadyMinGrade = configFile.codegenReadyMinGrade;
|
|
5790
5815
|
log(`Config loaded: ${options.config}`);
|
|
5791
5816
|
}
|
|
5817
|
+
const effectiveMinGrade = options.readyMinGrade ?? codegenReadyMinGrade;
|
|
5792
5818
|
let acknowledgments;
|
|
5793
5819
|
if (options.acknowledgments) {
|
|
5794
5820
|
const ackPath = resolve(options.acknowledgments);
|
|
@@ -5814,7 +5840,7 @@ Analyzing: ${file.name}`);
|
|
|
5814
5840
|
log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
|
|
5815
5841
|
const scores = calculateScores(result, configs);
|
|
5816
5842
|
if (options.json) {
|
|
5817
|
-
console.log(JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input) }), null, 2));
|
|
5843
|
+
console.log(JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input), ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {} }), null, 2));
|
|
5818
5844
|
if (scores.overall.grade === "F") {
|
|
5819
5845
|
process.exitCode = 1;
|
|
5820
5846
|
}
|
|
@@ -6006,7 +6032,13 @@ var BATCHABLE_RULE_IDS = [
|
|
|
6006
6032
|
"no-auto-layout",
|
|
6007
6033
|
"fixed-size-in-auto-layout"
|
|
6008
6034
|
];
|
|
6035
|
+
var OPT_IN_BATCHABLE_RULE_IDS = [
|
|
6036
|
+
"missing-prototype"
|
|
6037
|
+
];
|
|
6009
6038
|
var BATCHABLE_SET = new Set(BATCHABLE_RULE_IDS);
|
|
6039
|
+
var OPT_IN_BATCHABLE_SET = new Set(
|
|
6040
|
+
OPT_IN_BATCHABLE_RULE_IDS
|
|
6041
|
+
);
|
|
6010
6042
|
var NO_SOURCE_SENTINEL = "_no-source";
|
|
6011
6043
|
function groupAndBatchSurveyQuestions(questions) {
|
|
6012
6044
|
if (questions.length === 0) {
|
|
@@ -6047,20 +6079,25 @@ function sourceComponentKey(question) {
|
|
|
6047
6079
|
}
|
|
6048
6080
|
function pushIntoBatch(group, question) {
|
|
6049
6081
|
const sceneWeight = Math.max(question.replicas ?? 1, 1);
|
|
6050
|
-
const
|
|
6082
|
+
const batchMode = resolveBatchMode(question.ruleId);
|
|
6051
6083
|
const last = group.batches.at(-1);
|
|
6052
|
-
if (last !== void 0 && last.ruleId === question.ruleId &&
|
|
6084
|
+
if (last !== void 0 && last.ruleId === question.ruleId && batchMode !== "none" && last.batchMode === batchMode) {
|
|
6053
6085
|
last.questions.push(question);
|
|
6054
6086
|
last.totalScenes += sceneWeight;
|
|
6055
6087
|
return;
|
|
6056
6088
|
}
|
|
6057
6089
|
group.batches.push({
|
|
6058
6090
|
ruleId: question.ruleId,
|
|
6059
|
-
|
|
6091
|
+
batchMode,
|
|
6060
6092
|
questions: [question],
|
|
6061
6093
|
totalScenes: sceneWeight
|
|
6062
6094
|
});
|
|
6063
6095
|
}
|
|
6096
|
+
function resolveBatchMode(ruleId) {
|
|
6097
|
+
if (BATCHABLE_SET.has(ruleId)) return "safe";
|
|
6098
|
+
if (OPT_IN_BATCHABLE_SET.has(ruleId)) return "opt-in";
|
|
6099
|
+
return "none";
|
|
6100
|
+
}
|
|
6064
6101
|
|
|
6065
6102
|
// src/core/gotcha/survey-generator.ts
|
|
6066
6103
|
var NODE_PATH_SEPARATOR = " > ";
|
|
@@ -6079,12 +6116,18 @@ function generateGotchaSurvey(result, scores, options = {}) {
|
|
|
6079
6116
|
const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
6080
6117
|
const questions = deduplicateBySourceComponent(mapped);
|
|
6081
6118
|
const groupedQuestions = groupAndBatchSurveyQuestions(questions);
|
|
6119
|
+
const PROPAGATION_CANDIDATE_THRESHOLD = 3;
|
|
6120
|
+
const propagationCandidates = questions.filter(
|
|
6121
|
+
(q) => q.isInstanceChild
|
|
6122
|
+
).length;
|
|
6123
|
+
const suggestedDefaultApply = propagationCandidates >= PROPAGATION_CANDIDATE_THRESHOLD;
|
|
6082
6124
|
return {
|
|
6083
6125
|
designGrade: grade,
|
|
6084
|
-
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
6126
|
+
isReadyForCodeGen: isReadyForCodeGen(grade, options.codegenReadyMinGrade),
|
|
6085
6127
|
questions,
|
|
6086
6128
|
groupedQuestions,
|
|
6087
|
-
designKey: options.designKey ?? ""
|
|
6129
|
+
designKey: options.designKey ?? "",
|
|
6130
|
+
suggestedDefaultApply
|
|
6088
6131
|
};
|
|
6089
6132
|
}
|
|
6090
6133
|
function deduplicateSiblingIssues(issues) {
|
|
@@ -6233,26 +6276,36 @@ function findNodeById2(node, id) {
|
|
|
6233
6276
|
var GotchaSurveyOptionsSchema = z.object({
|
|
6234
6277
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional(),
|
|
6235
6278
|
token: z.string().optional(),
|
|
6279
|
+
/** Accepted for CLI parity with `analyze`; Figma URL loads always use REST (#461). */
|
|
6280
|
+
api: z.boolean().optional(),
|
|
6236
6281
|
config: z.string().optional(),
|
|
6237
6282
|
targetNodeId: z.string().optional(),
|
|
6238
6283
|
json: z.boolean().optional(),
|
|
6239
|
-
scope: z.enum(["page", "component"]).optional()
|
|
6284
|
+
scope: z.enum(["page", "component"]).optional(),
|
|
6285
|
+
readyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional()
|
|
6240
6286
|
});
|
|
6241
6287
|
async function runGotchaSurvey(input, options) {
|
|
6288
|
+
void options.api;
|
|
6242
6289
|
const { file, nodeId } = await loadFile(input, options.token);
|
|
6243
6290
|
const effectiveNodeId = options.targetNodeId ?? nodeId;
|
|
6244
6291
|
let configs = options.preset ? { ...getConfigsWithPreset(options.preset) } : { ...RULE_CONFIGS };
|
|
6292
|
+
let codegenReadyMinGrade;
|
|
6245
6293
|
if (options.config) {
|
|
6246
6294
|
const configFile = await loadConfigFile(options.config);
|
|
6247
6295
|
configs = mergeConfigs(configs, configFile);
|
|
6296
|
+
codegenReadyMinGrade = configFile.codegenReadyMinGrade;
|
|
6248
6297
|
}
|
|
6298
|
+
const effectiveMinGrade = options.readyMinGrade ?? codegenReadyMinGrade;
|
|
6249
6299
|
const result = analyzeFile(file, {
|
|
6250
6300
|
configs,
|
|
6251
6301
|
...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
|
|
6252
6302
|
...options.scope ? { scope: options.scope } : {}
|
|
6253
6303
|
});
|
|
6254
6304
|
const scores = calculateScores(result, configs);
|
|
6255
|
-
return generateGotchaSurvey(result, scores, {
|
|
6305
|
+
return generateGotchaSurvey(result, scores, {
|
|
6306
|
+
designKey: computeDesignKey(input),
|
|
6307
|
+
...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {}
|
|
6308
|
+
});
|
|
6256
6309
|
}
|
|
6257
6310
|
function formatHumanSummary(survey) {
|
|
6258
6311
|
const lines = [
|
|
@@ -6267,7 +6320,10 @@ function formatHumanSummary(survey) {
|
|
|
6267
6320
|
return lines.join("\n");
|
|
6268
6321
|
}
|
|
6269
6322
|
function registerGotchaSurvey(cli2) {
|
|
6270
|
-
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(
|
|
6323
|
+
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
|
+
"--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) => {
|
|
6271
6327
|
const parseResult = GotchaSurveyOptionsSchema.safeParse(rawOptions);
|
|
6272
6328
|
if (!parseResult.success) {
|
|
6273
6329
|
const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -6386,13 +6442,17 @@ var GotchaSurveyQuestionSchema = z.object({
|
|
|
6386
6442
|
var SurveyQuestionBatchSchema = z.object({
|
|
6387
6443
|
ruleId: z.string(),
|
|
6388
6444
|
/**
|
|
6389
|
-
*
|
|
6390
|
-
*
|
|
6391
|
-
*
|
|
6392
|
-
*
|
|
6393
|
-
*
|
|
6445
|
+
* Rendering mode for this batch (see `BatchMode` in
|
|
6446
|
+
* `src/core/gotcha/group-and-batch-questions.ts` — the authoritative
|
|
6447
|
+
* whitelists `BATCHABLE_RULE_IDS` and `OPT_IN_BATCHABLE_RULE_IDS` live
|
|
6448
|
+
* there):
|
|
6449
|
+
* - `"safe"` — one answer uniformly applies to every member (#369).
|
|
6450
|
+
* - `"opt-in"` — one shared answer is a suggested default; the user may
|
|
6451
|
+
* reply `split` for per-node override (#426).
|
|
6452
|
+
* - `"none"` — single-member batch, renders the standard per-question
|
|
6453
|
+
* template.
|
|
6394
6454
|
*/
|
|
6395
|
-
|
|
6455
|
+
batchMode: z.enum(["safe", "opt-in", "none"]),
|
|
6396
6456
|
questions: z.array(GotchaSurveyQuestionSchema),
|
|
6397
6457
|
/**
|
|
6398
6458
|
* Sum of `max(question.replicas, 1)` across `questions`. Counts the
|
|
@@ -6426,7 +6486,21 @@ var GotchaSurveySchema = z.object({
|
|
|
6426
6486
|
* this directly when upserting the per-design section, so the SKILL.md
|
|
6427
6487
|
* prose no longer parses URLs (per ADR-016).
|
|
6428
6488
|
*/
|
|
6429
|
-
designKey: z.string()
|
|
6489
|
+
designKey: z.string(),
|
|
6490
|
+
/**
|
|
6491
|
+
* #428 — threshold hint for the `allowDefinitionWrite` picker in the
|
|
6492
|
+
* `canicode-roundtrip` skill. `true` when `propagationCandidates >= 3`
|
|
6493
|
+
* (i.e. three or more questions target instance children that could
|
|
6494
|
+
* benefit from definition-level writes). When `false`, the skill silently
|
|
6495
|
+
* uses the annotation default (ADR-012) without surfacing the picker —
|
|
6496
|
+
* the opt-in flow is over-engineered for tiny surveys.
|
|
6497
|
+
*
|
|
6498
|
+
* Computed server-side from `questions` so the skill doesn't have to
|
|
6499
|
+
* count `isInstanceChild` manually; the skill may still override this
|
|
6500
|
+
* hint when it has additional context (e.g. all candidates are
|
|
6501
|
+
* read-only per probe result).
|
|
6502
|
+
*/
|
|
6503
|
+
suggestedDefaultApply: z.boolean()
|
|
6430
6504
|
});
|
|
6431
6505
|
var AnswersMapSchema = z.record(
|
|
6432
6506
|
z.string(),
|
|
@@ -6908,52 +6982,6 @@ function defaultSourceDir() {
|
|
|
6908
6982
|
function defaultCursorBundleRoot() {
|
|
6909
6983
|
return fileURLToPath(new URL("../../skills/cursor", import.meta.url));
|
|
6910
6984
|
}
|
|
6911
|
-
async function copySkillTree(skillName, srcSkillDir, destSkillDir, force) {
|
|
6912
|
-
if (!existsSync(srcSkillDir)) {
|
|
6913
|
-
throw new Error(`Bundled skill directory missing: ${srcSkillDir}`);
|
|
6914
|
-
}
|
|
6915
|
-
mkdirSync(destSkillDir, { recursive: true });
|
|
6916
|
-
const ops = [];
|
|
6917
|
-
const files = listFilesRecursive(srcSkillDir);
|
|
6918
|
-
for (const relPath of files) {
|
|
6919
|
-
const src = join(srcSkillDir, relPath);
|
|
6920
|
-
const dest = join(destSkillDir, relPath);
|
|
6921
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
6922
|
-
const label = join(skillName, relPath);
|
|
6923
|
-
let action;
|
|
6924
|
-
if (!existsSync(dest)) {
|
|
6925
|
-
action = "install";
|
|
6926
|
-
} else if (force) {
|
|
6927
|
-
action = "force-overwrite";
|
|
6928
|
-
} else {
|
|
6929
|
-
action = "needs-decision";
|
|
6930
|
-
}
|
|
6931
|
-
ops.push({ src, dest, label, action });
|
|
6932
|
-
}
|
|
6933
|
-
const candidates = ops.filter((op) => op.action === "needs-decision");
|
|
6934
|
-
const decisions = candidates.length > 0 ? await promptOverwriteBatch(candidates.map((op) => ({ label: op.label, dest: op.dest }))) : /* @__PURE__ */ new Map();
|
|
6935
|
-
const installed = [];
|
|
6936
|
-
const overwritten = [];
|
|
6937
|
-
const skipped = [];
|
|
6938
|
-
for (const op of ops) {
|
|
6939
|
-
if (op.action === "install") {
|
|
6940
|
-
copyFileSync(op.src, op.dest);
|
|
6941
|
-
installed.push(op.label);
|
|
6942
|
-
} else if (op.action === "force-overwrite") {
|
|
6943
|
-
copyFileSync(op.src, op.dest);
|
|
6944
|
-
overwritten.push(op.label);
|
|
6945
|
-
} else {
|
|
6946
|
-
const decision = decisions.get(op.label) ?? "skip";
|
|
6947
|
-
if (decision === "overwrite") {
|
|
6948
|
-
copyFileSync(op.src, op.dest);
|
|
6949
|
-
overwritten.push(op.label);
|
|
6950
|
-
} else {
|
|
6951
|
-
skipped.push(op.label);
|
|
6952
|
-
}
|
|
6953
|
-
}
|
|
6954
|
-
}
|
|
6955
|
-
return { installed, overwritten, skipped };
|
|
6956
|
-
}
|
|
6957
6985
|
async function copyMultipleSkillTrees(entries, force) {
|
|
6958
6986
|
const ops = [];
|
|
6959
6987
|
for (const { skillName, srcSkillDir, destSkillDir } of entries) {
|
|
@@ -7064,35 +7092,6 @@ If you installed canicode from npm, please file a bug report \u2014 the tarball
|
|
|
7064
7092
|
}
|
|
7065
7093
|
return summary;
|
|
7066
7094
|
}
|
|
7067
|
-
var InstallClaudeGotchasOnlySchema = z.object({
|
|
7068
|
-
force: z.boolean(),
|
|
7069
|
-
cwd: z.string().optional(),
|
|
7070
|
-
sourceDir: z.string().optional()
|
|
7071
|
-
});
|
|
7072
|
-
async function installClaudeGotchasSkillOnly(rawOptions) {
|
|
7073
|
-
const options = InstallClaudeGotchasOnlySchema.parse(rawOptions);
|
|
7074
|
-
const sourceDir = options.sourceDir ?? defaultSourceDir();
|
|
7075
|
-
const skillName = "canicode-gotchas";
|
|
7076
|
-
const srcSkillDir = join(sourceDir, skillName);
|
|
7077
|
-
const cwd = options.cwd ?? process.cwd();
|
|
7078
|
-
const targetDir = join(cwd, ".claude", "skills");
|
|
7079
|
-
const destSkillDir = join(targetDir, skillName);
|
|
7080
|
-
if (!existsSync(sourceDir)) {
|
|
7081
|
-
throw new Error(
|
|
7082
|
-
`Bundled skills directory not found: ${sourceDir}
|
|
7083
|
-
If you are developing canicode, run 'pnpm build' first.
|
|
7084
|
-
If you installed canicode from npm, please file a bug report \u2014 the tarball is missing skills/.`
|
|
7085
|
-
);
|
|
7086
|
-
}
|
|
7087
|
-
mkdirSync(targetDir, { recursive: true });
|
|
7088
|
-
const part = await copySkillTree(skillName, srcSkillDir, destSkillDir, options.force);
|
|
7089
|
-
return {
|
|
7090
|
-
installed: part.installed,
|
|
7091
|
-
overwritten: part.overwritten,
|
|
7092
|
-
skipped: part.skipped,
|
|
7093
|
-
targetDir
|
|
7094
|
-
};
|
|
7095
|
-
}
|
|
7096
7095
|
var InstallCursorBundledSchema = z.object({
|
|
7097
7096
|
force: z.boolean(),
|
|
7098
7097
|
cwd: z.string().optional(),
|
|
@@ -7252,14 +7251,67 @@ function formatNextSteps(opts) {
|
|
|
7252
7251
|
var InitOptionsSchema = z.object({
|
|
7253
7252
|
token: z.string().optional(),
|
|
7254
7253
|
global: z.boolean().optional(),
|
|
7255
|
-
// Declared positively as `--skills`; mri's built-in `--no-` prefix handling
|
|
7256
|
-
// still maps `--no-skills` to `skills: false`. Declaring the option
|
|
7257
|
-
// positively avoids cac's `(default: true)` artifact on negated flags.
|
|
7258
|
-
skills: z.boolean().optional(),
|
|
7259
7254
|
/** Install `skills/cursor/*` into `.cursor/skills/` (canicode, gotchas, roundtrip — issue #407). */
|
|
7260
7255
|
cursorSkills: z.boolean().optional(),
|
|
7261
7256
|
force: z.boolean().optional()
|
|
7262
7257
|
});
|
|
7258
|
+
function wantsSkillInstallWithoutToken(options) {
|
|
7259
|
+
return options.cursorSkills === true;
|
|
7260
|
+
}
|
|
7261
|
+
async function runInitSkillInstallSteps(options) {
|
|
7262
|
+
let skillStepOk = true;
|
|
7263
|
+
let skillSummary;
|
|
7264
|
+
try {
|
|
7265
|
+
const summary = await installSkills({
|
|
7266
|
+
target: options.global ? "global" : "project",
|
|
7267
|
+
force: options.force ?? false
|
|
7268
|
+
});
|
|
7269
|
+
console.log(`
|
|
7270
|
+
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}`);
|
|
7274
|
+
if (summary.skipped.length > 0) {
|
|
7275
|
+
console.log(` (Re-run with --force to overwrite skipped files.)`);
|
|
7276
|
+
}
|
|
7277
|
+
skillSummary = {
|
|
7278
|
+
installed: summary.installed.length,
|
|
7279
|
+
overwritten: summary.overwritten.length,
|
|
7280
|
+
skipped: summary.skipped.length
|
|
7281
|
+
};
|
|
7282
|
+
} catch (skillError) {
|
|
7283
|
+
console.error(
|
|
7284
|
+
`
|
|
7285
|
+
Skill install failed: ${skillError instanceof Error ? skillError.message : String(skillError)}`
|
|
7286
|
+
);
|
|
7287
|
+
process.exitCode = 1;
|
|
7288
|
+
skillStepOk = false;
|
|
7289
|
+
}
|
|
7290
|
+
if (options.cursorSkills && skillStepOk) {
|
|
7291
|
+
try {
|
|
7292
|
+
const cSummary = await installCursorBundledSkills({
|
|
7293
|
+
force: options.force ?? false
|
|
7294
|
+
});
|
|
7295
|
+
console.log(`
|
|
7296
|
+
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}`);
|
|
7300
|
+
if (cSummary.skipped.length > 0) {
|
|
7301
|
+
console.log(` (Re-run with --force to overwrite skipped files.)`);
|
|
7302
|
+
}
|
|
7303
|
+
console.log(` Open a new chat and @-mention canicode, canicode-gotchas, or canicode-roundtrip if skills do not appear immediately.`);
|
|
7304
|
+
} catch (cursorError) {
|
|
7305
|
+
console.error(
|
|
7306
|
+
`
|
|
7307
|
+
Cursor skill install failed: ${cursorError instanceof Error ? cursorError.message : String(cursorError)}`
|
|
7308
|
+
);
|
|
7309
|
+
process.exitCode = 1;
|
|
7310
|
+
skillStepOk = false;
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
return skillSummary !== void 0 ? { skillStepOk, skillSummary } : { skillStepOk };
|
|
7314
|
+
}
|
|
7263
7315
|
function registerInit(cli2) {
|
|
7264
7316
|
cli2.command(
|
|
7265
7317
|
"init",
|
|
@@ -7267,7 +7319,7 @@ function registerInit(cli2) {
|
|
|
7267
7319
|
).option(
|
|
7268
7320
|
"--token <token>",
|
|
7269
7321
|
"Save Figma API token (use env/CLI only \u2014 not agent chat) and install Claude Code skills to .claude/skills/"
|
|
7270
|
-
).option("--global", "Install skills to ~/.claude/skills/ instead of ./.claude/skills/").option("--
|
|
7322
|
+
).option("--global", "Install skills to ~/.claude/skills/ instead of ./.claude/skills/").option("--cursor-skills", "Also install Cursor copies of canicode / canicode-gotchas / canicode-roundtrip under .cursor/skills/ (with `--token`, runs after Claude skills; without token, installs Claude skills + Cursor bundle)").option("--force", "Overwrite existing skill files without prompting (also for non-TTY/CI)").action(async (rawOptions) => {
|
|
7271
7323
|
try {
|
|
7272
7324
|
const parseResult = InitOptionsSchema.safeParse(rawOptions);
|
|
7273
7325
|
if (!parseResult.success) {
|
|
@@ -7282,98 +7334,48 @@ ${msg}`);
|
|
|
7282
7334
|
initAiready(options.token);
|
|
7283
7335
|
console.log(` Config saved: ${getConfigPath()}`);
|
|
7284
7336
|
console.log(` Reports will be saved to: ${getReportsDir()}/`);
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
}
|
|
7301
|
-
|
|
7302
|
-
installed: summary.installed.length,
|
|
7303
|
-
overwritten: summary.overwritten.length,
|
|
7304
|
-
skipped: summary.skipped.length
|
|
7305
|
-
};
|
|
7306
|
-
} catch (skillError) {
|
|
7307
|
-
console.error(
|
|
7308
|
-
`
|
|
7309
|
-
Skill install failed: ${skillError instanceof Error ? skillError.message : String(skillError)}`
|
|
7310
|
-
);
|
|
7311
|
-
process.exitCode = 1;
|
|
7312
|
-
skillStepOk = false;
|
|
7313
|
-
}
|
|
7314
|
-
} else if (options.cursorSkills) {
|
|
7315
|
-
try {
|
|
7316
|
-
const summary = await installClaudeGotchasSkillOnly({
|
|
7317
|
-
force: options.force ?? false
|
|
7318
|
-
});
|
|
7319
|
-
console.log(`
|
|
7320
|
-
Gotchas store (Claude Code skills path) installed to: ${summary.targetDir}/`);
|
|
7321
|
-
console.log(` installed: ${summary.installed.length}`);
|
|
7322
|
-
console.log(` overwritten: ${summary.overwritten.length}`);
|
|
7323
|
-
console.log(` skipped: ${summary.skipped.length}`);
|
|
7324
|
-
skillSummary = {
|
|
7325
|
-
installed: summary.installed.length,
|
|
7326
|
-
overwritten: summary.overwritten.length,
|
|
7327
|
-
skipped: summary.skipped.length
|
|
7328
|
-
};
|
|
7329
|
-
} catch (skillError) {
|
|
7330
|
-
console.error(
|
|
7331
|
-
`
|
|
7332
|
-
Gotchas skill install failed: ${skillError instanceof Error ? skillError.message : String(skillError)}`
|
|
7333
|
-
);
|
|
7334
|
-
process.exitCode = 1;
|
|
7335
|
-
skillStepOk = false;
|
|
7336
|
-
}
|
|
7337
|
-
}
|
|
7338
|
-
if (options.cursorSkills && skillStepOk) {
|
|
7339
|
-
try {
|
|
7340
|
-
const cSummary = await installCursorBundledSkills({
|
|
7341
|
-
force: options.force ?? false
|
|
7342
|
-
});
|
|
7343
|
-
console.log(`
|
|
7344
|
-
Cursor skills installed to: ${cSummary.targetDir}/`);
|
|
7345
|
-
console.log(` installed: ${cSummary.installed.length}`);
|
|
7346
|
-
console.log(` overwritten: ${cSummary.overwritten.length}`);
|
|
7347
|
-
console.log(` skipped: ${cSummary.skipped.length}`);
|
|
7348
|
-
if (cSummary.skipped.length > 0) {
|
|
7349
|
-
console.log(` (Re-run with --force to overwrite skipped files.)`);
|
|
7350
|
-
}
|
|
7351
|
-
console.log(` Open a new chat and @-mention canicode, canicode-gotchas, or canicode-roundtrip if skills do not appear immediately.`);
|
|
7352
|
-
} catch (cursorError) {
|
|
7353
|
-
console.error(
|
|
7354
|
-
`
|
|
7355
|
-
Cursor skill install failed: ${cursorError instanceof Error ? cursorError.message : String(cursorError)}`
|
|
7356
|
-
);
|
|
7357
|
-
process.exitCode = 1;
|
|
7358
|
-
skillStepOk = false;
|
|
7359
|
-
}
|
|
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
|
+
);
|
|
7360
7354
|
}
|
|
7355
|
+
return;
|
|
7356
|
+
}
|
|
7357
|
+
if (wantsSkillInstallWithoutToken(options)) {
|
|
7358
|
+
const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
|
|
7361
7359
|
trackEvent(EVENTS.CLI_INIT, {
|
|
7362
|
-
skillsRequested:
|
|
7360
|
+
skillsRequested: true,
|
|
7363
7361
|
cursorSkillsRequested: options.cursorSkills === true,
|
|
7364
7362
|
skillStepOk,
|
|
7365
7363
|
target: options.global ? "global" : "project",
|
|
7366
7364
|
force: options.force ?? false,
|
|
7365
|
+
skillOnlyInit: true,
|
|
7367
7366
|
...skillSummary ?? {}
|
|
7368
7367
|
});
|
|
7369
7368
|
if (skillStepOk) {
|
|
7370
7369
|
console.log(
|
|
7371
7370
|
formatNextSteps({
|
|
7372
7371
|
figmaMcpPresent: figmaMcpRegistered(),
|
|
7373
|
-
skillsInstalled:
|
|
7372
|
+
skillsInstalled: true,
|
|
7374
7373
|
cursorSkillsInstalled: options.cursorSkills === true
|
|
7375
7374
|
})
|
|
7376
7375
|
);
|
|
7376
|
+
console.log(
|
|
7377
|
+
"\n Figma token not saved \u2014 run `canicode init --token \u2026` when you need REST analyze or MCP against live files."
|
|
7378
|
+
);
|
|
7377
7379
|
}
|
|
7378
7380
|
return;
|
|
7379
7381
|
}
|
|
@@ -7390,8 +7392,7 @@ ${msg}`);
|
|
|
7390
7392
|
console.log(` --token also installs three Claude Code skills into ./.claude/skills/`);
|
|
7391
7393
|
console.log(` (canicode, canicode-gotchas, canicode-roundtrip).`);
|
|
7392
7394
|
console.log(` --global Install to ~/.claude/skills/ instead`);
|
|
7393
|
-
console.log(` --
|
|
7394
|
-
console.log(` --cursor-skills Also install Cursor copies of all three skills (.cursor/skills/); with --no-skills, still installs .claude gotcha store + Cursor bundle`);
|
|
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)`);
|
|
7395
7396
|
console.log(` --force Overwrite existing skill files without prompting
|
|
7396
7397
|
`);
|
|
7397
7398
|
console.log(`After setup:`);
|
|
@@ -10818,10 +10819,7 @@ cli.help((sections) => {
|
|
|
10818
10819
|
},
|
|
10819
10820
|
{
|
|
10820
10821
|
title: "\nData source",
|
|
10821
|
-
body:
|
|
10822
|
-
` --api Load via Figma REST API (needs FIGMA_TOKEN)`,
|
|
10823
|
-
` --token <token> Figma API token (or use FIGMA_TOKEN env var)`
|
|
10824
|
-
].join("\n")
|
|
10822
|
+
body: ` --token <token> Figma API token (or use FIGMA_TOKEN env var)`
|
|
10825
10823
|
},
|
|
10826
10824
|
{
|
|
10827
10825
|
title: "\nCustomization",
|
|
@@ -10830,7 +10828,6 @@ cli.help((sections) => {
|
|
|
10830
10828
|
{
|
|
10831
10829
|
title: "\nExamples",
|
|
10832
10830
|
body: [
|
|
10833
|
-
` $ canicode analyze "https://www.figma.com/design/..." --api`,
|
|
10834
10831
|
` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
|
|
10835
10832
|
` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`,
|
|
10836
10833
|
` $ canicode gotcha-survey "https://www.figma.com/design/..." --json`,
|