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 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. `--no-skills` skips skill install (token only). `--force` overwrites existing skill files without prompting. Run `canicode docs setup` for the full setup guide.
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 (and installs the Claude Code skills as a bonus — pass `--no-skills` to skip).
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, copyFileSync, readdirSync, renameSync, chmodSync } from 'fs';
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 skills \u2014 see section 2 for --no-skills)
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
- --no-skills Skip skill install (token only \u2014 legacy behavior)
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
- --no-skills Skip Claude Code skills (with --cursor-skills, still installs
1608
- the Cursor bundle plus the shared gotchas answer file)
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.1";
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("--api", "Load via Figma REST API (requires FIGMA_TOKEN)").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 https://www.figma.com/design/ABC123/MyDesign --api --token YOUR_TOKEN").example(" canicode analyze ./fixtures/my-design --output report.html").example(" canicode analyze ./fixtures/my-design --config ./my-config.json").action(async (input, rawOptions) => {
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 isBatchable = BATCHABLE_SET.has(question.ruleId);
6074
+ const batchMode = resolveBatchMode(question.ruleId);
6051
6075
  const last = group.batches.at(-1);
6052
- if (last !== void 0 && last.ruleId === question.ruleId && isBatchable && last.batchable) {
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
- batchable: isBatchable,
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("--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) => {
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
- * `true` when every member shares an answer-shape uniformly applicable to
6390
- * all of them (e.g. one `min-width` value covers all FILL children).
6391
- * The SKILL renders one shared prompt for `batchable: true` batches with
6392
- * `questions.length >= 2`; everything else falls through to the
6393
- * single-question template.
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
- batchable: z.boolean(),
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("--skills", "Install Claude Code skills into .claude/skills/ (default: on \u2014 pass --no-skills to opt out)").option("--cursor-skills", "Also install Cursor copies of canicode / canicode-gotchas / canicode-roundtrip under .cursor/skills/").option("--force", "Overwrite existing skill files without prompting (also for non-TTY/CI)").action(async (rawOptions) => {
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
- let skillStepOk = true;
7286
- let skillSummary;
7287
- if (options.skills !== false) {
7288
- try {
7289
- const summary = await installSkills({
7290
- target: options.global ? "global" : "project",
7291
- force: options.force ?? false
7292
- });
7293
- console.log(`
7294
- Skills installed to: ${summary.targetDir}/`);
7295
- console.log(` installed: ${summary.installed.length}`);
7296
- console.log(` overwritten: ${summary.overwritten.length}`);
7297
- console.log(` skipped: ${summary.skipped.length}`);
7298
- if (summary.skipped.length > 0) {
7299
- console.log(` (Re-run with --force to overwrite skipped files.)`);
7300
- }
7301
- skillSummary = {
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: options.skills !== false,
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: options.skills !== false,
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(` --no-skills Skip skill install (token only)`);
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`,