canicode 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, writeFileSync, readFileSync, statSync, readdirSync, renameSync, chmodSync, copyFileSync } from 'fs';
3
- import { join, resolve, dirname, basename, relative } from 'path';
3
+ import { join, resolve, dirname, basename, isAbsolute, sep, relative } from 'path';
4
4
  import pixelmatch from 'pixelmatch';
5
5
  import { PNG } from 'pngjs';
6
6
  import { createRequire } from 'module';
@@ -132,6 +132,32 @@ var init_figma_client = __esm({
132
132
  const buffer = await response.arrayBuffer();
133
133
  return Buffer.from(buffer).toString("base64");
134
134
  }
135
+ /**
136
+ * Get the components a file has published to a team library.
137
+ *
138
+ * `GET /v1/files/:file_key/components` returns only components that have
139
+ * been pushed via the Publish Library action — local-but-unpublished
140
+ * components are absent. This is the authoritative way to detect whether
141
+ * a Figma component is mappable via Code Connect (#532): `add_code_connect_map`
142
+ * requires a published component and otherwise fails with "Published
143
+ * component not found."
144
+ */
145
+ async getPublishedComponents(fileKey) {
146
+ const url = `${FIGMA_API_BASE}/files/${fileKey}/components`;
147
+ const response = await fetch(url, {
148
+ headers: { "X-Figma-Token": this.token }
149
+ });
150
+ if (!response.ok) {
151
+ const error = await response.json().catch(() => ({}));
152
+ throw new FigmaClientError(
153
+ `Failed to fetch published components: ${response.status} ${response.statusText}`,
154
+ response.status,
155
+ error
156
+ );
157
+ }
158
+ const data = await response.json();
159
+ return data.meta?.components ?? [];
160
+ }
135
161
  async getFileNodes(fileKey, nodeIds) {
136
162
  const ids = nodeIds.join(",");
137
163
  const url = `${FIGMA_API_BASE}/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}`;
@@ -3053,6 +3079,176 @@ defineRule({
3053
3079
  definition: irregularSpacingDef,
3054
3080
  check: irregularSpacingCheck
3055
3081
  });
3082
+ var FIGMA_CONFIG_FILENAME = "figma.config.json";
3083
+ var FIGMA_CONNECT_FILE_GLOB = /\.figma\.(tsx?|jsx?)$/;
3084
+ var NODE_ID_QUERY_RE = /[?&]node-id=([0-9A-Za-z%:\-_]+)/;
3085
+ function parseCodeConnectMappings(cwd) {
3086
+ const configPath = join(cwd, FIGMA_CONFIG_FILENAME);
3087
+ if (!existsSync(configPath)) {
3088
+ return {
3089
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3090
+ scannedFiles: [],
3091
+ skipReason: "no-config",
3092
+ skippedReason: `${FIGMA_CONFIG_FILENAME} not found at ${cwd}`
3093
+ };
3094
+ }
3095
+ let config2;
3096
+ try {
3097
+ config2 = JSON.parse(readFileSync(configPath, "utf-8"));
3098
+ } catch (err) {
3099
+ return {
3100
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3101
+ scannedFiles: [],
3102
+ skipReason: "malformed-config",
3103
+ skippedReason: `malformed ${FIGMA_CONFIG_FILENAME}: ${err.message}`
3104
+ };
3105
+ }
3106
+ const includes = config2.codeConnect?.include ?? config2.include ?? [];
3107
+ if (includes.length === 0) {
3108
+ return {
3109
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3110
+ scannedFiles: [],
3111
+ skipReason: "no-includes",
3112
+ skippedReason: `${FIGMA_CONFIG_FILENAME} has no codeConnect.include paths`
3113
+ };
3114
+ }
3115
+ const candidateFiles = /* @__PURE__ */ new Set();
3116
+ for (const includePattern of includes) {
3117
+ for (const file of resolveInclude(cwd, includePattern)) {
3118
+ candidateFiles.add(file);
3119
+ }
3120
+ }
3121
+ const mappedNodeIds = /* @__PURE__ */ new Set();
3122
+ const scannedFiles = [];
3123
+ for (const file of candidateFiles) {
3124
+ scannedFiles.push(file);
3125
+ let contents;
3126
+ try {
3127
+ contents = readFileSync(file, "utf-8");
3128
+ } catch {
3129
+ continue;
3130
+ }
3131
+ for (const nodeId of extractNodeIdsFromSource(contents)) {
3132
+ mappedNodeIds.add(nodeId);
3133
+ }
3134
+ }
3135
+ return { mappedNodeIds, scannedFiles };
3136
+ }
3137
+ function resolveInclude(cwd, includePattern) {
3138
+ const results = [];
3139
+ const absolute = isAbsolute(includePattern) ? includePattern : resolve(cwd, includePattern);
3140
+ const segments = absolute.split(sep);
3141
+ let firstGlobIdx = segments.findIndex((s) => /[*?{[]/.test(s));
3142
+ if (firstGlobIdx === -1) {
3143
+ if (existsSync(absolute)) {
3144
+ const stat = statSync(absolute);
3145
+ if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(absolute)) {
3146
+ results.push(absolute);
3147
+ } else if (stat.isDirectory()) {
3148
+ walkDir(absolute, results);
3149
+ }
3150
+ }
3151
+ return results;
3152
+ }
3153
+ const rootSegments = segments.slice(0, firstGlobIdx);
3154
+ const root = rootSegments.length === 0 ? sep : rootSegments.join(sep);
3155
+ if (!existsSync(root)) return results;
3156
+ const rootStat = statSync(root);
3157
+ if (!rootStat.isDirectory()) return results;
3158
+ walkDir(root, results);
3159
+ const prefix = rootSegments.join(sep) + sep;
3160
+ return results.filter((f) => f.startsWith(prefix) || rootSegments.length === 0);
3161
+ }
3162
+ function walkDir(dir, out) {
3163
+ let entries;
3164
+ try {
3165
+ entries = readdirSync(dir);
3166
+ } catch {
3167
+ return;
3168
+ }
3169
+ for (const entry of entries) {
3170
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
3171
+ const full = join(dir, entry);
3172
+ let stat;
3173
+ try {
3174
+ stat = statSync(full);
3175
+ } catch {
3176
+ continue;
3177
+ }
3178
+ if (stat.isDirectory()) {
3179
+ walkDir(full, out);
3180
+ } else if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(full)) {
3181
+ out.push(full);
3182
+ }
3183
+ }
3184
+ }
3185
+ function extractNodeIdsFromSource(source) {
3186
+ const nodeIds = /* @__PURE__ */ new Set();
3187
+ const re = new RegExp(NODE_ID_QUERY_RE, "g");
3188
+ let match;
3189
+ while ((match = re.exec(source)) !== null) {
3190
+ const raw = match[1];
3191
+ if (!raw) continue;
3192
+ const decoded = safeDecode(raw);
3193
+ nodeIds.add(decoded.replace(/-/g, ":"));
3194
+ }
3195
+ return nodeIds;
3196
+ }
3197
+ function safeDecode(raw) {
3198
+ try {
3199
+ return decodeURIComponent(raw);
3200
+ } catch {
3201
+ return raw;
3202
+ }
3203
+ }
3204
+ var PropertyAcknowledgmentIntentSchema = z.object({
3205
+ kind: z.literal("property").default("property"),
3206
+ field: z.string(),
3207
+ value: z.unknown(),
3208
+ scope: z.enum(["instance", "definition"])
3209
+ });
3210
+ var RuleOptOutAcknowledgmentIntentSchema = z.object({
3211
+ kind: z.literal("rule-opt-out"),
3212
+ ruleId: z.string()
3213
+ }).strict();
3214
+ var AcknowledgmentIntentSchema = z.preprocess((raw) => {
3215
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
3216
+ const obj = raw;
3217
+ if (obj["kind"] === void 0) {
3218
+ return { ...obj, kind: "property" };
3219
+ }
3220
+ }
3221
+ return raw;
3222
+ }, z.discriminatedUnion("kind", [
3223
+ PropertyAcknowledgmentIntentSchema,
3224
+ RuleOptOutAcknowledgmentIntentSchema
3225
+ ]));
3226
+ function isRuleOptOutIntent(intent) {
3227
+ return intent !== void 0 && intent.kind === "rule-opt-out";
3228
+ }
3229
+ var AcknowledgmentSceneWriteOutcomeSchema = z.object({
3230
+ result: z.enum([
3231
+ "succeeded",
3232
+ "silent-ignored",
3233
+ "api-rejected",
3234
+ "user-declined-propagation",
3235
+ "unknown"
3236
+ ]),
3237
+ reason: z.string().optional()
3238
+ });
3239
+ var AcknowledgmentSchema = z.object({
3240
+ nodeId: z.string(),
3241
+ ruleId: z.string(),
3242
+ intent: AcknowledgmentIntentSchema.optional(),
3243
+ sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
3244
+ codegenDirective: z.string().optional()
3245
+ });
3246
+ var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
3247
+ function normalizeNodeId(id) {
3248
+ return id.replace(/-/g, ":");
3249
+ }
3250
+
3251
+ // src/core/rules/component/index.ts
3056
3252
  var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
3057
3253
  function detectStyleOverrides(master, instance) {
3058
3254
  const overrides = [];
@@ -3261,11 +3457,19 @@ defineRule({
3261
3457
  check: variantStructureMismatchCheck
3262
3458
  });
3263
3459
  var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
3460
+ var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
3264
3461
  function codeConnectIsSetUp(context) {
3265
3462
  return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3266
3463
  return existsSync(join(process.cwd(), "figma.config.json"));
3267
3464
  });
3268
3465
  }
3466
+ function codeConnectMappings(context) {
3467
+ return getAnalysisState(
3468
+ context,
3469
+ CODE_CONNECT_MAPPINGS_KEY,
3470
+ () => parseCodeConnectMappings(process.cwd())
3471
+ );
3472
+ }
3269
3473
  var unmappedComponentDef = {
3270
3474
  id: "unmapped-component",
3271
3475
  name: "Unmapped Component",
@@ -3278,6 +3482,12 @@ var unmappedComponentCheck = (node, context) => {
3278
3482
  if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3279
3483
  if (isInsideInstance(context)) return null;
3280
3484
  if (!codeConnectIsSetUp(context)) return null;
3485
+ const mappings = codeConnectMappings(context);
3486
+ if (mappings.mappedNodeIds.has(node.id)) return null;
3487
+ const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
3488
+ if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
3489
+ return null;
3490
+ }
3281
3491
  return {
3282
3492
  ruleId: unmappedComponentDef.id,
3283
3493
  nodeId: node.id,
@@ -3645,32 +3855,6 @@ defineRule({
3645
3855
  definition: missingPrototypeDef,
3646
3856
  check: missingPrototypeCheck
3647
3857
  });
3648
- var AcknowledgmentIntentSchema = z.object({
3649
- field: z.string(),
3650
- value: z.unknown(),
3651
- scope: z.enum(["instance", "definition"])
3652
- });
3653
- var AcknowledgmentSceneWriteOutcomeSchema = z.object({
3654
- result: z.enum([
3655
- "succeeded",
3656
- "silent-ignored",
3657
- "api-rejected",
3658
- "user-declined-propagation",
3659
- "unknown"
3660
- ]),
3661
- reason: z.string().optional()
3662
- });
3663
- var AcknowledgmentSchema = z.object({
3664
- nodeId: z.string(),
3665
- ruleId: z.string(),
3666
- intent: AcknowledgmentIntentSchema.optional(),
3667
- sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
3668
- codegenDirective: z.string().optional()
3669
- });
3670
- var AcknowledgmentListSchema = z.array(AcknowledgmentSchema);
3671
- function normalizeNodeId(id) {
3672
- return id.replace(/-/g, ":");
3673
- }
3674
3858
  var AnalysisScopeSchema = z.enum(["page", "component"]);
3675
3859
  var COMPONENT_SCOPE_ROOT_TYPES = /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET", "INSTANCE"]);
3676
3860
  function detectAnalysisScope(rootNode) {
@@ -3727,6 +3911,7 @@ var RuleEngine = class {
3727
3911
  excludeNamePattern;
3728
3912
  excludeNodeTypes;
3729
3913
  acknowledgments;
3914
+ acknowledgmentsByKey;
3730
3915
  scopeOverride;
3731
3916
  constructor(options = {}) {
3732
3917
  this.configs = options.configs ?? RULE_CONFIGS;
@@ -3735,10 +3920,15 @@ var RuleEngine = class {
3735
3920
  this.targetNodeId = options.targetNodeId;
3736
3921
  this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
3737
3922
  this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
3923
+ const ackList = options.acknowledgments ?? [];
3738
3924
  this.acknowledgments = new Set(
3739
- (options.acknowledgments ?? []).map(
3740
- (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
3741
- )
3925
+ ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
3926
+ );
3927
+ this.acknowledgmentsByKey = new Map(
3928
+ ackList.map((a) => [
3929
+ `${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
3930
+ a
3931
+ ])
3742
3932
  );
3743
3933
  this.scopeOverride = options.scope;
3744
3934
  }
@@ -3822,6 +4012,7 @@ var RuleEngine = class {
3822
4012
  if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
3823
4013
  return;
3824
4014
  }
4015
+ const acknowledgmentsByKey = this.acknowledgmentsByKey;
3825
4016
  const context = {
3826
4017
  file,
3827
4018
  parent,
@@ -3833,7 +4024,8 @@ var RuleEngine = class {
3833
4024
  siblings,
3834
4025
  analysisState,
3835
4026
  scope,
3836
- rootNodeType
4027
+ rootNodeType,
4028
+ findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
3837
4029
  };
3838
4030
  for (const rule of rules) {
3839
4031
  const ruleId = rule.definition.id;
@@ -4317,7 +4509,7 @@ function computeApplyContext(violation, instanceContext) {
4317
4509
  }
4318
4510
 
4319
4511
  // package.json
4320
- var version2 = "0.12.0";
4512
+ var version2 = "0.12.1";
4321
4513
 
4322
4514
  // src/core/engine/scoring.ts
4323
4515
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -4509,6 +4701,19 @@ function formatScoreSummary(report) {
4509
4701
  }
4510
4702
  return lines.join("\n");
4511
4703
  }
4704
+ function formatCodeConnectCoverageLine(coverage) {
4705
+ const { mapped, total } = coverage;
4706
+ const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
4707
+ return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
4708
+ }
4709
+ var ROUNDTRIP_OPT_OUT_HINT = "Some components may carry roundtrip-recorded opt-outs that this standalone analyze cannot see (Figma REST annotations field is in private beta). Run /canicode-roundtrip to apply opt-outs.";
4710
+ function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
4711
+ const hasUnmapped = issues.some(
4712
+ (issue) => issue.violation.ruleId === "unmapped-component"
4713
+ );
4714
+ if (!hasUnmapped) return null;
4715
+ return ROUNDTRIP_OPT_OUT_HINT;
4716
+ }
4512
4717
  function buildResultJson(fileName, result, scores, options) {
4513
4718
  const issuesByRule = {};
4514
4719
  for (const issue of result.issues) {
@@ -4538,6 +4743,14 @@ function buildResultJson(fileName, result, scores, options) {
4538
4743
  ...issue.acknowledged === true ? { acknowledged: true } : {}
4539
4744
  };
4540
4745
  });
4746
+ const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues) : null;
4747
+ const summaryParts = [formatScoreSummary(scores)];
4748
+ if (options?.codeConnectCoverage) {
4749
+ summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
4750
+ }
4751
+ if (optOutHint) {
4752
+ summaryParts.push(optOutHint);
4753
+ }
4541
4754
  const json = {
4542
4755
  version: version2,
4543
4756
  analyzedAt: result.analyzedAt,
@@ -4557,13 +4770,31 @@ function buildResultJson(fileName, result, scores, options) {
4557
4770
  },
4558
4771
  issuesByRule,
4559
4772
  issues,
4560
- summary: formatScoreSummary(scores)
4773
+ summary: summaryParts.join("\n\n")
4561
4774
  };
4775
+ if (options?.codeConnectCoverage) {
4776
+ json["codeConnectCoverage"] = options.codeConnectCoverage;
4777
+ }
4778
+ if (optOutHint) {
4779
+ json["roundtripOptOutHint"] = optOutHint;
4780
+ }
4562
4781
  if (result.failedRules.length > 0) {
4563
4782
  json["failedRules"] = result.failedRules;
4564
4783
  }
4565
4784
  return json;
4566
4785
  }
4786
+
4787
+ // src/core/rules/component/code-connect-coverage.ts
4788
+ function computeCodeConnectCoverage(components, cwd = process.cwd()) {
4789
+ const result = parseCodeConnectMappings(cwd);
4790
+ if (result.skipReason === "no-config") return void 0;
4791
+ const componentNodeIds = Object.keys(components);
4792
+ let mapped = 0;
4793
+ for (const nodeId of componentNodeIds) {
4794
+ if (result.mappedNodeIds.has(nodeId)) mapped++;
4795
+ }
4796
+ return { mapped, total: componentNodeIds.length };
4797
+ }
4567
4798
  function isFigmaUrl2(input) {
4568
4799
  return input.includes("figma.com/");
4569
4800
  }
@@ -5922,8 +6153,17 @@ Analyzing: ${file.name}`);
5922
6153
  const result = analyzeFile(file, analyzeOptions);
5923
6154
  log(`Nodes: ${result.nodeCount} (max depth: ${result.maxDepth})`);
5924
6155
  const scores = calculateScores(result, configs);
6156
+ const coverage = computeCodeConnectCoverage(file.components);
6157
+ const optOutHintEligible = acknowledgments === void 0;
6158
+ const optOutHint = optOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
5925
6159
  if (options.json) {
5926
- console.log(JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input), ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {} }), null, 2));
6160
+ console.log(JSON.stringify(buildResultJson(file.name, result, scores, {
6161
+ fileKey: file.fileKey,
6162
+ designKey: computeDesignKey(input),
6163
+ ...effectiveMinGrade ? { codegenReadyMinGrade: effectiveMinGrade } : {},
6164
+ ...coverage ? { codeConnectCoverage: coverage } : {},
6165
+ roundtripOptOutHintEligible: optOutHintEligible
6166
+ }), null, 2));
5927
6167
  if (scores.overall.grade === "F") {
5928
6168
  process.exitCode = 1;
5929
6169
  }
@@ -5931,6 +6171,14 @@ Analyzing: ${file.name}`);
5931
6171
  }
5932
6172
  console.log("\n" + "=".repeat(50));
5933
6173
  console.log(formatScoreSummary(scores));
6174
+ if (coverage) {
6175
+ console.log("");
6176
+ console.log(formatCodeConnectCoverageLine(coverage));
6177
+ }
6178
+ if (optOutHint) {
6179
+ console.log("");
6180
+ console.log(optOutHint);
6181
+ }
5934
6182
  console.log("=".repeat(50));
5935
6183
  const now = /* @__PURE__ */ new Date();
5936
6184
  const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}`;
@@ -6785,8 +7033,8 @@ function renderUpsertedFile(args) {
6785
7033
  }
6786
7034
  let working = currentContent;
6787
7035
  if (state === "missing-heading") {
6788
- const sep = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
6789
- working = `${working}${sep}${COLLECTED_GOTCHAS_HEADING}
7036
+ const sep2 = working.endsWith("\n\n") ? "" : working.endsWith("\n") ? "\n" : "\n\n";
7037
+ working = `${working}${sep2}${COLLECTED_GOTCHAS_HEADING}
6790
7038
  `;
6791
7039
  }
6792
7040
  const plan = findOrAppendSection(working, designKey);
@@ -7645,6 +7893,9 @@ function registerConfig(cli2) {
7645
7893
  }
7646
7894
  });
7647
7895
  }
7896
+
7897
+ // src/cli/commands/doctor.ts
7898
+ init_figma_client();
7648
7899
  var CODE_CONNECT_PKG = "@figma/code-connect";
7649
7900
  var CODE_CONNECT_DOCS = "https://www.figma.com/code-connect-docs/";
7650
7901
  function readPackageJson(cwd) {
@@ -7692,10 +7943,85 @@ function runCodeConnectChecks(cwd) {
7692
7943
  }
7693
7944
  return results;
7694
7945
  }
7946
+ var PUBLISH_CHECK_NAME = "Figma component published in a library";
7947
+ async function runFigmaPublishCheck(input) {
7948
+ const { figmaUrl, token, fetchPublishedComponents } = input;
7949
+ let parsed;
7950
+ try {
7951
+ parsed = parseFigmaUrl(figmaUrl);
7952
+ } catch (err) {
7953
+ const message = err instanceof FigmaUrlParseError ? err.message : String(err);
7954
+ return {
7955
+ name: PUBLISH_CHECK_NAME,
7956
+ pass: false,
7957
+ inconclusive: true,
7958
+ detail: `could not parse URL: ${message}`,
7959
+ remediation: "Pass a valid Figma design URL (figma.com/design/<file>?node-id=<id>)."
7960
+ };
7961
+ }
7962
+ if (!parsed.nodeId) {
7963
+ return {
7964
+ name: PUBLISH_CHECK_NAME,
7965
+ pass: false,
7966
+ inconclusive: true,
7967
+ detail: "URL is missing a node-id",
7968
+ remediation: "Code Connect mapping is per-component \u2014 invoke with a URL that targets a specific node (?node-id=\u2026)."
7969
+ };
7970
+ }
7971
+ if (!token) {
7972
+ return {
7973
+ name: PUBLISH_CHECK_NAME,
7974
+ pass: false,
7975
+ inconclusive: true,
7976
+ detail: "FIGMA_TOKEN not configured \u2014 skipping publish-status check",
7977
+ remediation: "Set FIGMA_TOKEN (env var) or run `canicode config set-token` so doctor can verify this prereq inline."
7978
+ };
7979
+ }
7980
+ if (!fetchPublishedComponents) {
7981
+ return {
7982
+ name: PUBLISH_CHECK_NAME,
7983
+ pass: false,
7984
+ inconclusive: true,
7985
+ detail: "no fetcher wired",
7986
+ remediation: "internal: doctor was called without a Figma client"
7987
+ };
7988
+ }
7989
+ let components;
7990
+ try {
7991
+ components = await fetchPublishedComponents(parsed.fileKey);
7992
+ } catch (err) {
7993
+ const status = err instanceof FigmaClientError ? err.statusCode : void 0;
7994
+ const message = err instanceof Error ? err.message : String(err);
7995
+ return {
7996
+ name: PUBLISH_CHECK_NAME,
7997
+ pass: false,
7998
+ inconclusive: true,
7999
+ detail: `Figma API call failed${status ? ` (HTTP ${status})` : ""}: ${message}`,
8000
+ remediation: "Step 7d will rely on the API as the authority; if your token / network is OK, the canicode-roundtrip step itself will surface the publish error inline."
8001
+ };
8002
+ }
8003
+ const canonicalNodeId = parsed.nodeId.replace(/-/g, ":");
8004
+ const match = components.find(
8005
+ (c) => c.node_id === canonicalNodeId || c.node_id === parsed.nodeId
8006
+ );
8007
+ if (match) {
8008
+ return {
8009
+ name: PUBLISH_CHECK_NAME,
8010
+ pass: true,
8011
+ detail: `${match.name} (${match.node_id})`
8012
+ };
8013
+ }
8014
+ return {
8015
+ name: PUBLISH_CHECK_NAME,
8016
+ pass: false,
8017
+ detail: `node ${canonicalNodeId} is not in the published-components list for file ${parsed.fileKey}`,
8018
+ remediation: "Open the file in Figma \u2192 Assets panel \u2192 Publish library and include this component. Without publishing, `add_code_connect_map` fails with 'Published component not found.'"
8019
+ };
8020
+ }
7695
8021
  function formatDoctorReport(results) {
7696
8022
  const lines = ["Code Connect"];
7697
8023
  for (const result of results) {
7698
- const icon = result.pass ? "\u2705" : "\u274C";
8024
+ const icon = result.pass ? "\u2705" : result.inconclusive ? "\u26A0\uFE0F" : "\u274C";
7699
8025
  const detail = result.detail ? ` (${result.detail})` : "";
7700
8026
  lines.push(` ${icon} ${result.name}${detail}`);
7701
8027
  if (!result.pass && result.remediation) {
@@ -7703,22 +8029,43 @@ function formatDoctorReport(results) {
7703
8029
  }
7704
8030
  }
7705
8031
  lines.push("");
7706
- const allPass = results.every((r) => r.pass);
7707
- lines.push(
7708
- allPass ? "All checks passed." : "Some checks failed. Fix the items above before running the Code Connect flow."
7709
- );
8032
+ const blocking = results.filter((r) => !r.pass && !r.inconclusive).length;
8033
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8034
+ if (blocking === 0 && inconclusive === 0) {
8035
+ lines.push("All checks passed.");
8036
+ } else if (blocking === 0) {
8037
+ lines.push(
8038
+ "Blocking checks passed; some checks were skipped (\u26A0\uFE0F) and could not be verified."
8039
+ );
8040
+ } else {
8041
+ lines.push("Some checks failed. Fix the items above before running the Code Connect flow.");
8042
+ }
7710
8043
  return lines.join("\n");
7711
8044
  }
7712
8045
  function registerDoctor(cli2) {
7713
8046
  cli2.command(
7714
8047
  "doctor",
7715
8048
  "Diagnose Code Connect prerequisites (`@figma/code-connect`, `figma.config.json`)"
7716
- ).action(() => {
8049
+ ).option(
8050
+ "--figma-url <url>",
8051
+ "Optionally check that the target Figma component is published in a library (requires FIGMA_TOKEN)"
8052
+ ).action(async (options) => {
7717
8053
  const cwd = process.cwd();
7718
8054
  const results = runCodeConnectChecks(cwd);
8055
+ if (options.figmaUrl) {
8056
+ const token = getFigmaToken();
8057
+ const client = token ? new FigmaClient({ token }) : void 0;
8058
+ const publishCheck = await runFigmaPublishCheck({
8059
+ figmaUrl: options.figmaUrl,
8060
+ token,
8061
+ fetchPublishedComponents: client ? (fileKey) => client.getPublishedComponents(fileKey) : void 0
8062
+ });
8063
+ results.push(publishCheck);
8064
+ }
7719
8065
  console.log(formatDoctorReport(results));
7720
8066
  const passed = results.filter((r) => r.pass).length;
7721
- const failed = results.length - passed;
8067
+ const inconclusive = results.filter((r) => !r.pass && r.inconclusive).length;
8068
+ const failed = results.length - passed - inconclusive;
7722
8069
  trackEvent(EVENTS.CLI_DOCTOR, {
7723
8070
  passed,
7724
8071
  failed,
@@ -10856,8 +11203,8 @@ ${msg}`);
10856
11203
  if (vectorDir && existsSync(vectorDir)) {
10857
11204
  const vecOutputDir = resolve(outputDir, "vectors");
10858
11205
  mkdirSync(vecOutputDir, { recursive: true });
10859
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10860
- const vecFiles = readdirSync4(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
11206
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11207
+ const vecFiles = readdirSync5(vectorDir).filter((f) => f.endsWith(".svg") || f === "mapping.json");
10861
11208
  for (const f of vecFiles) {
10862
11209
  copyFileSync3(resolve(vectorDir, f), resolve(vecOutputDir, f));
10863
11210
  }
@@ -10867,8 +11214,8 @@ ${msg}`);
10867
11214
  if (imageDir && existsSync(imageDir)) {
10868
11215
  const imgOutputDir = resolve(outputDir, "images");
10869
11216
  mkdirSync(imgOutputDir, { recursive: true });
10870
- const { readdirSync: readdirSync4, copyFileSync: copyFileSync3 } = await import('fs');
10871
- const imgFiles = readdirSync4(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
11217
+ const { readdirSync: readdirSync5, copyFileSync: copyFileSync3 } = await import('fs');
11218
+ const imgFiles = readdirSync5(imageDir).filter((f) => f.endsWith(".png") || f.endsWith(".jpg") || f.endsWith(".json"));
10872
11219
  for (const f of imgFiles) {
10873
11220
  copyFileSync3(resolve(imageDir, f), resolve(imgOutputDir, f));
10874
11221
  }