canicode 0.11.1 → 0.11.2
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 +165 -183
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +22 -5
- package/dist/index.js +26 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +22 -5
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +38 -2
- package/package.json +2 -2
- package/skills/canicode-gotchas/SKILL.md +53 -7
- package/skills/canicode-roundtrip/SKILL.md +49 -2
- 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 +53 -7
- package/skills/cursor/canicode-roundtrip/SKILL.md +49 -2
- 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,7 +4245,7 @@ function computeApplyContext(violation, instanceContext) {
|
|
|
4232
4245
|
}
|
|
4233
4246
|
|
|
4234
4247
|
// package.json
|
|
4235
|
-
var version2 = "0.11.
|
|
4248
|
+
var version2 = "0.11.2";
|
|
4236
4249
|
|
|
4237
4250
|
// src/core/engine/scoring.ts
|
|
4238
4251
|
function computeTotalScorePerCategory(configs) {
|
|
@@ -5711,6 +5724,7 @@ var AnalyzeOptionsSchema = z.object({
|
|
|
5711
5724
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional(),
|
|
5712
5725
|
output: z.string().optional(),
|
|
5713
5726
|
token: z.string().optional(),
|
|
5727
|
+
/** Accepted for CLI parity; Figma URL loads always use the REST API today (#461). */
|
|
5714
5728
|
api: z.boolean().optional(),
|
|
5715
5729
|
screenshot: z.boolean().optional(),
|
|
5716
5730
|
config: z.string().optional(),
|
|
@@ -5720,7 +5734,10 @@ var AnalyzeOptionsSchema = z.object({
|
|
|
5720
5734
|
scope: z.enum(["page", "component"]).optional()
|
|
5721
5735
|
});
|
|
5722
5736
|
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(
|
|
5737
|
+
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(
|
|
5738
|
+
"--api",
|
|
5739
|
+
"No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `gotcha-survey` (#461)."
|
|
5740
|
+
).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.").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
5741
|
const parseResult = AnalyzeOptionsSchema.safeParse(rawOptions);
|
|
5725
5742
|
if (!parseResult.success) {
|
|
5726
5743
|
const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -5730,6 +5747,7 @@ ${msg}`);
|
|
|
5730
5747
|
process.exit(1);
|
|
5731
5748
|
}
|
|
5732
5749
|
const options = parseResult.data;
|
|
5750
|
+
void options.api;
|
|
5733
5751
|
const analysisStart = Date.now();
|
|
5734
5752
|
trackEvent(EVENTS.ANALYSIS_STARTED, { source: isJsonFile(input) || isFixtureDir(input) ? "fixture" : "figma" });
|
|
5735
5753
|
const log = options.json ? console.error.bind(console) : console.log.bind(console);
|
|
@@ -6006,7 +6024,13 @@ var BATCHABLE_RULE_IDS = [
|
|
|
6006
6024
|
"no-auto-layout",
|
|
6007
6025
|
"fixed-size-in-auto-layout"
|
|
6008
6026
|
];
|
|
6027
|
+
var OPT_IN_BATCHABLE_RULE_IDS = [
|
|
6028
|
+
"missing-prototype"
|
|
6029
|
+
];
|
|
6009
6030
|
var BATCHABLE_SET = new Set(BATCHABLE_RULE_IDS);
|
|
6031
|
+
var OPT_IN_BATCHABLE_SET = new Set(
|
|
6032
|
+
OPT_IN_BATCHABLE_RULE_IDS
|
|
6033
|
+
);
|
|
6010
6034
|
var NO_SOURCE_SENTINEL = "_no-source";
|
|
6011
6035
|
function groupAndBatchSurveyQuestions(questions) {
|
|
6012
6036
|
if (questions.length === 0) {
|
|
@@ -6047,20 +6071,25 @@ function sourceComponentKey(question) {
|
|
|
6047
6071
|
}
|
|
6048
6072
|
function pushIntoBatch(group, question) {
|
|
6049
6073
|
const sceneWeight = Math.max(question.replicas ?? 1, 1);
|
|
6050
|
-
const
|
|
6074
|
+
const batchMode = resolveBatchMode(question.ruleId);
|
|
6051
6075
|
const last = group.batches.at(-1);
|
|
6052
|
-
if (last !== void 0 && last.ruleId === question.ruleId &&
|
|
6076
|
+
if (last !== void 0 && last.ruleId === question.ruleId && batchMode !== "none" && last.batchMode === batchMode) {
|
|
6053
6077
|
last.questions.push(question);
|
|
6054
6078
|
last.totalScenes += sceneWeight;
|
|
6055
6079
|
return;
|
|
6056
6080
|
}
|
|
6057
6081
|
group.batches.push({
|
|
6058
6082
|
ruleId: question.ruleId,
|
|
6059
|
-
|
|
6083
|
+
batchMode,
|
|
6060
6084
|
questions: [question],
|
|
6061
6085
|
totalScenes: sceneWeight
|
|
6062
6086
|
});
|
|
6063
6087
|
}
|
|
6088
|
+
function resolveBatchMode(ruleId) {
|
|
6089
|
+
if (BATCHABLE_SET.has(ruleId)) return "safe";
|
|
6090
|
+
if (OPT_IN_BATCHABLE_SET.has(ruleId)) return "opt-in";
|
|
6091
|
+
return "none";
|
|
6092
|
+
}
|
|
6064
6093
|
|
|
6065
6094
|
// src/core/gotcha/survey-generator.ts
|
|
6066
6095
|
var NODE_PATH_SEPARATOR = " > ";
|
|
@@ -6079,12 +6108,18 @@ function generateGotchaSurvey(result, scores, options = {}) {
|
|
|
6079
6108
|
const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
|
|
6080
6109
|
const questions = deduplicateBySourceComponent(mapped);
|
|
6081
6110
|
const groupedQuestions = groupAndBatchSurveyQuestions(questions);
|
|
6111
|
+
const PROPAGATION_CANDIDATE_THRESHOLD = 3;
|
|
6112
|
+
const propagationCandidates = questions.filter(
|
|
6113
|
+
(q) => q.isInstanceChild
|
|
6114
|
+
).length;
|
|
6115
|
+
const suggestedDefaultApply = propagationCandidates >= PROPAGATION_CANDIDATE_THRESHOLD;
|
|
6082
6116
|
return {
|
|
6083
6117
|
designGrade: grade,
|
|
6084
6118
|
isReadyForCodeGen: isReadyForCodeGen(grade),
|
|
6085
6119
|
questions,
|
|
6086
6120
|
groupedQuestions,
|
|
6087
|
-
designKey: options.designKey ?? ""
|
|
6121
|
+
designKey: options.designKey ?? "",
|
|
6122
|
+
suggestedDefaultApply
|
|
6088
6123
|
};
|
|
6089
6124
|
}
|
|
6090
6125
|
function deduplicateSiblingIssues(issues) {
|
|
@@ -6233,12 +6268,15 @@ function findNodeById2(node, id) {
|
|
|
6233
6268
|
var GotchaSurveyOptionsSchema = z.object({
|
|
6234
6269
|
preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional(),
|
|
6235
6270
|
token: z.string().optional(),
|
|
6271
|
+
/** Accepted for CLI parity with `analyze`; Figma URL loads always use REST (#461). */
|
|
6272
|
+
api: z.boolean().optional(),
|
|
6236
6273
|
config: z.string().optional(),
|
|
6237
6274
|
targetNodeId: z.string().optional(),
|
|
6238
6275
|
json: z.boolean().optional(),
|
|
6239
6276
|
scope: z.enum(["page", "component"]).optional()
|
|
6240
6277
|
});
|
|
6241
6278
|
async function runGotchaSurvey(input, options) {
|
|
6279
|
+
void options.api;
|
|
6242
6280
|
const { file, nodeId } = await loadFile(input, options.token);
|
|
6243
6281
|
const effectiveNodeId = options.targetNodeId ?? nodeId;
|
|
6244
6282
|
let configs = options.preset ? { ...getConfigsWithPreset(options.preset) } : { ...RULE_CONFIGS };
|
|
@@ -6267,7 +6305,10 @@ function formatHumanSummary(survey) {
|
|
|
6267
6305
|
return lines.join("\n");
|
|
6268
6306
|
}
|
|
6269
6307
|
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(
|
|
6308
|
+
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(
|
|
6309
|
+
"--api",
|
|
6310
|
+
"No-op for Figma URL inputs (file data is always fetched via REST). Accepted for CLI parity with `analyze` (#461)."
|
|
6311
|
+
).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)").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
6312
|
const parseResult = GotchaSurveyOptionsSchema.safeParse(rawOptions);
|
|
6272
6313
|
if (!parseResult.success) {
|
|
6273
6314
|
const msg = parseResult.error.issues.map((i) => `--${i.path.join(".")}: ${i.message}`).join("\n");
|
|
@@ -6386,13 +6427,17 @@ var GotchaSurveyQuestionSchema = z.object({
|
|
|
6386
6427
|
var SurveyQuestionBatchSchema = z.object({
|
|
6387
6428
|
ruleId: z.string(),
|
|
6388
6429
|
/**
|
|
6389
|
-
*
|
|
6390
|
-
*
|
|
6391
|
-
*
|
|
6392
|
-
*
|
|
6393
|
-
*
|
|
6430
|
+
* Rendering mode for this batch (see `BatchMode` in
|
|
6431
|
+
* `src/core/gotcha/group-and-batch-questions.ts` — the authoritative
|
|
6432
|
+
* whitelists `BATCHABLE_RULE_IDS` and `OPT_IN_BATCHABLE_RULE_IDS` live
|
|
6433
|
+
* there):
|
|
6434
|
+
* - `"safe"` — one answer uniformly applies to every member (#369).
|
|
6435
|
+
* - `"opt-in"` — one shared answer is a suggested default; the user may
|
|
6436
|
+
* reply `split` for per-node override (#426).
|
|
6437
|
+
* - `"none"` — single-member batch, renders the standard per-question
|
|
6438
|
+
* template.
|
|
6394
6439
|
*/
|
|
6395
|
-
|
|
6440
|
+
batchMode: z.enum(["safe", "opt-in", "none"]),
|
|
6396
6441
|
questions: z.array(GotchaSurveyQuestionSchema),
|
|
6397
6442
|
/**
|
|
6398
6443
|
* Sum of `max(question.replicas, 1)` across `questions`. Counts the
|
|
@@ -6426,7 +6471,21 @@ var GotchaSurveySchema = z.object({
|
|
|
6426
6471
|
* this directly when upserting the per-design section, so the SKILL.md
|
|
6427
6472
|
* prose no longer parses URLs (per ADR-016).
|
|
6428
6473
|
*/
|
|
6429
|
-
designKey: z.string()
|
|
6474
|
+
designKey: z.string(),
|
|
6475
|
+
/**
|
|
6476
|
+
* #428 — threshold hint for the `allowDefinitionWrite` picker in the
|
|
6477
|
+
* `canicode-roundtrip` skill. `true` when `propagationCandidates >= 3`
|
|
6478
|
+
* (i.e. three or more questions target instance children that could
|
|
6479
|
+
* benefit from definition-level writes). When `false`, the skill silently
|
|
6480
|
+
* uses the annotation default (ADR-012) without surfacing the picker —
|
|
6481
|
+
* the opt-in flow is over-engineered for tiny surveys.
|
|
6482
|
+
*
|
|
6483
|
+
* Computed server-side from `questions` so the skill doesn't have to
|
|
6484
|
+
* count `isInstanceChild` manually; the skill may still override this
|
|
6485
|
+
* hint when it has additional context (e.g. all candidates are
|
|
6486
|
+
* read-only per probe result).
|
|
6487
|
+
*/
|
|
6488
|
+
suggestedDefaultApply: z.boolean()
|
|
6430
6489
|
});
|
|
6431
6490
|
var AnswersMapSchema = z.record(
|
|
6432
6491
|
z.string(),
|
|
@@ -6908,52 +6967,6 @@ function defaultSourceDir() {
|
|
|
6908
6967
|
function defaultCursorBundleRoot() {
|
|
6909
6968
|
return fileURLToPath(new URL("../../skills/cursor", import.meta.url));
|
|
6910
6969
|
}
|
|
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
6970
|
async function copyMultipleSkillTrees(entries, force) {
|
|
6958
6971
|
const ops = [];
|
|
6959
6972
|
for (const { skillName, srcSkillDir, destSkillDir } of entries) {
|
|
@@ -7064,35 +7077,6 @@ If you installed canicode from npm, please file a bug report \u2014 the tarball
|
|
|
7064
7077
|
}
|
|
7065
7078
|
return summary;
|
|
7066
7079
|
}
|
|
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
7080
|
var InstallCursorBundledSchema = z.object({
|
|
7097
7081
|
force: z.boolean(),
|
|
7098
7082
|
cwd: z.string().optional(),
|
|
@@ -7252,14 +7236,67 @@ function formatNextSteps(opts) {
|
|
|
7252
7236
|
var InitOptionsSchema = z.object({
|
|
7253
7237
|
token: z.string().optional(),
|
|
7254
7238
|
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
7239
|
/** Install `skills/cursor/*` into `.cursor/skills/` (canicode, gotchas, roundtrip — issue #407). */
|
|
7260
7240
|
cursorSkills: z.boolean().optional(),
|
|
7261
7241
|
force: z.boolean().optional()
|
|
7262
7242
|
});
|
|
7243
|
+
function wantsSkillInstallWithoutToken(options) {
|
|
7244
|
+
return options.cursorSkills === true;
|
|
7245
|
+
}
|
|
7246
|
+
async function runInitSkillInstallSteps(options) {
|
|
7247
|
+
let skillStepOk = true;
|
|
7248
|
+
let skillSummary;
|
|
7249
|
+
try {
|
|
7250
|
+
const summary = await installSkills({
|
|
7251
|
+
target: options.global ? "global" : "project",
|
|
7252
|
+
force: options.force ?? false
|
|
7253
|
+
});
|
|
7254
|
+
console.log(`
|
|
7255
|
+
Skills installed to: ${summary.targetDir}/`);
|
|
7256
|
+
console.log(` installed: ${summary.installed.length}`);
|
|
7257
|
+
console.log(` overwritten: ${summary.overwritten.length}`);
|
|
7258
|
+
console.log(` skipped: ${summary.skipped.length}`);
|
|
7259
|
+
if (summary.skipped.length > 0) {
|
|
7260
|
+
console.log(` (Re-run with --force to overwrite skipped files.)`);
|
|
7261
|
+
}
|
|
7262
|
+
skillSummary = {
|
|
7263
|
+
installed: summary.installed.length,
|
|
7264
|
+
overwritten: summary.overwritten.length,
|
|
7265
|
+
skipped: summary.skipped.length
|
|
7266
|
+
};
|
|
7267
|
+
} catch (skillError) {
|
|
7268
|
+
console.error(
|
|
7269
|
+
`
|
|
7270
|
+
Skill install failed: ${skillError instanceof Error ? skillError.message : String(skillError)}`
|
|
7271
|
+
);
|
|
7272
|
+
process.exitCode = 1;
|
|
7273
|
+
skillStepOk = false;
|
|
7274
|
+
}
|
|
7275
|
+
if (options.cursorSkills && skillStepOk) {
|
|
7276
|
+
try {
|
|
7277
|
+
const cSummary = await installCursorBundledSkills({
|
|
7278
|
+
force: options.force ?? false
|
|
7279
|
+
});
|
|
7280
|
+
console.log(`
|
|
7281
|
+
Cursor skills installed to: ${cSummary.targetDir}/`);
|
|
7282
|
+
console.log(` installed: ${cSummary.installed.length}`);
|
|
7283
|
+
console.log(` overwritten: ${cSummary.overwritten.length}`);
|
|
7284
|
+
console.log(` skipped: ${cSummary.skipped.length}`);
|
|
7285
|
+
if (cSummary.skipped.length > 0) {
|
|
7286
|
+
console.log(` (Re-run with --force to overwrite skipped files.)`);
|
|
7287
|
+
}
|
|
7288
|
+
console.log(` Open a new chat and @-mention canicode, canicode-gotchas, or canicode-roundtrip if skills do not appear immediately.`);
|
|
7289
|
+
} catch (cursorError) {
|
|
7290
|
+
console.error(
|
|
7291
|
+
`
|
|
7292
|
+
Cursor skill install failed: ${cursorError instanceof Error ? cursorError.message : String(cursorError)}`
|
|
7293
|
+
);
|
|
7294
|
+
process.exitCode = 1;
|
|
7295
|
+
skillStepOk = false;
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
return skillSummary !== void 0 ? { skillStepOk, skillSummary } : { skillStepOk };
|
|
7299
|
+
}
|
|
7263
7300
|
function registerInit(cli2) {
|
|
7264
7301
|
cli2.command(
|
|
7265
7302
|
"init",
|
|
@@ -7267,7 +7304,7 @@ function registerInit(cli2) {
|
|
|
7267
7304
|
).option(
|
|
7268
7305
|
"--token <token>",
|
|
7269
7306
|
"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("--
|
|
7307
|
+
).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
7308
|
try {
|
|
7272
7309
|
const parseResult = InitOptionsSchema.safeParse(rawOptions);
|
|
7273
7310
|
if (!parseResult.success) {
|
|
@@ -7282,98 +7319,48 @@ ${msg}`);
|
|
|
7282
7319
|
initAiready(options.token);
|
|
7283
7320
|
console.log(` Config saved: ${getConfigPath()}`);
|
|
7284
7321
|
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
|
-
}
|
|
7322
|
+
const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
|
|
7323
|
+
trackEvent(EVENTS.CLI_INIT, {
|
|
7324
|
+
skillsRequested: true,
|
|
7325
|
+
cursorSkillsRequested: options.cursorSkills === true,
|
|
7326
|
+
skillStepOk,
|
|
7327
|
+
target: options.global ? "global" : "project",
|
|
7328
|
+
force: options.force ?? false,
|
|
7329
|
+
...skillSummary ?? {}
|
|
7330
|
+
});
|
|
7331
|
+
if (skillStepOk) {
|
|
7332
|
+
console.log(
|
|
7333
|
+
formatNextSteps({
|
|
7334
|
+
figmaMcpPresent: figmaMcpRegistered(),
|
|
7335
|
+
skillsInstalled: true,
|
|
7336
|
+
cursorSkillsInstalled: options.cursorSkills === true
|
|
7337
|
+
})
|
|
7338
|
+
);
|
|
7360
7339
|
}
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7342
|
+
if (wantsSkillInstallWithoutToken(options)) {
|
|
7343
|
+
const { skillStepOk, skillSummary } = await runInitSkillInstallSteps(options);
|
|
7361
7344
|
trackEvent(EVENTS.CLI_INIT, {
|
|
7362
|
-
skillsRequested:
|
|
7345
|
+
skillsRequested: true,
|
|
7363
7346
|
cursorSkillsRequested: options.cursorSkills === true,
|
|
7364
7347
|
skillStepOk,
|
|
7365
7348
|
target: options.global ? "global" : "project",
|
|
7366
7349
|
force: options.force ?? false,
|
|
7350
|
+
skillOnlyInit: true,
|
|
7367
7351
|
...skillSummary ?? {}
|
|
7368
7352
|
});
|
|
7369
7353
|
if (skillStepOk) {
|
|
7370
7354
|
console.log(
|
|
7371
7355
|
formatNextSteps({
|
|
7372
7356
|
figmaMcpPresent: figmaMcpRegistered(),
|
|
7373
|
-
skillsInstalled:
|
|
7357
|
+
skillsInstalled: true,
|
|
7374
7358
|
cursorSkillsInstalled: options.cursorSkills === true
|
|
7375
7359
|
})
|
|
7376
7360
|
);
|
|
7361
|
+
console.log(
|
|
7362
|
+
"\n Figma token not saved \u2014 run `canicode init --token \u2026` when you need REST analyze or MCP against live files."
|
|
7363
|
+
);
|
|
7377
7364
|
}
|
|
7378
7365
|
return;
|
|
7379
7366
|
}
|
|
@@ -7390,8 +7377,7 @@ ${msg}`);
|
|
|
7390
7377
|
console.log(` --token also installs three Claude Code skills into ./.claude/skills/`);
|
|
7391
7378
|
console.log(` (canicode, canicode-gotchas, canicode-roundtrip).`);
|
|
7392
7379
|
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`);
|
|
7380
|
+
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
7381
|
console.log(` --force Overwrite existing skill files without prompting
|
|
7396
7382
|
`);
|
|
7397
7383
|
console.log(`After setup:`);
|
|
@@ -10818,10 +10804,7 @@ cli.help((sections) => {
|
|
|
10818
10804
|
},
|
|
10819
10805
|
{
|
|
10820
10806
|
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")
|
|
10807
|
+
body: ` --token <token> Figma API token (or use FIGMA_TOKEN env var)`
|
|
10825
10808
|
},
|
|
10826
10809
|
{
|
|
10827
10810
|
title: "\nCustomization",
|
|
@@ -10830,7 +10813,6 @@ cli.help((sections) => {
|
|
|
10830
10813
|
{
|
|
10831
10814
|
title: "\nExamples",
|
|
10832
10815
|
body: [
|
|
10833
|
-
` $ canicode analyze "https://www.figma.com/design/..." --api`,
|
|
10834
10816
|
` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
|
|
10835
10817
|
` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`,
|
|
10836
10818
|
` $ canicode gotcha-survey "https://www.figma.com/design/..." --json`,
|