canicode 0.10.2 → 0.10.4

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.
@@ -615,6 +615,14 @@ function defineRule(rule) {
615
615
  ruleRegistry.register(rule);
616
616
  return rule;
617
617
  }
618
+ var AcknowledgmentSchema = z.object({
619
+ nodeId: z.string(),
620
+ ruleId: z.string()
621
+ });
622
+ z.array(AcknowledgmentSchema);
623
+ function normalizeNodeId(id) {
624
+ return id.replace(/-/g, ":");
625
+ }
618
626
 
619
627
  // src/core/engine/rule-engine.ts
620
628
  function calculateMaxDepth(node, currentDepth = 0) {
@@ -665,6 +673,7 @@ var RuleEngine = class {
665
673
  targetNodeId;
666
674
  excludeNamePattern;
667
675
  excludeNodeTypes;
676
+ acknowledgments;
668
677
  constructor(options = {}) {
669
678
  this.configs = options.configs ?? RULE_CONFIGS;
670
679
  this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
@@ -672,6 +681,11 @@ var RuleEngine = class {
672
681
  this.targetNodeId = options.targetNodeId;
673
682
  this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
674
683
  this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
684
+ this.acknowledgments = new Set(
685
+ (options.acknowledgments ?? []).map(
686
+ (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
687
+ )
688
+ );
675
689
  }
676
690
  /**
677
691
  * Analyze a Figma file and return issues
@@ -706,6 +720,14 @@ var RuleEngine = class {
706
720
  void 0,
707
721
  void 0
708
722
  );
723
+ if (this.acknowledgments.size > 0) {
724
+ for (const issue of issues) {
725
+ const key = `${normalizeNodeId(issue.violation.nodeId)}::${issue.violation.ruleId}`;
726
+ if (this.acknowledgments.has(key)) {
727
+ issue.acknowledged = true;
728
+ }
729
+ }
730
+ }
709
731
  return {
710
732
  file,
711
733
  issues,
@@ -1708,8 +1730,6 @@ function resolveTargetProperty(ruleId, subType) {
1708
1730
  if (subType === "horizontal") return "layoutSizingHorizontal";
1709
1731
  return ["layoutSizingHorizontal", "layoutSizingVertical"];
1710
1732
  case "missing-size-constraint":
1711
- if (subType === "wrap") return "minWidth";
1712
- if (subType === "max-width") return "maxWidth";
1713
1733
  return ["minWidth", "maxWidth"];
1714
1734
  case "irregular-spacing":
1715
1735
  if (subType === "gap") return "itemSpacing";
@@ -1753,7 +1773,7 @@ function computeApplyContext(violation, instanceContext) {
1753
1773
  }
1754
1774
 
1755
1775
  // package.json
1756
- var version = "0.10.2";
1776
+ var version = "0.10.4";
1757
1777
 
1758
1778
  // src/core/engine/scoring.ts
1759
1779
  function computeTotalScorePerCategory(configs) {
@@ -1809,7 +1829,8 @@ function calculateScores(result, configs) {
1809
1829
  uniqueRulesPerCategory.get(category).add(ruleId);
1810
1830
  ruleScorePerCategory.get(category).set(ruleId, Math.abs(issue.config.score));
1811
1831
  const ruleCountMap = ruleIssueCountPerCategory.get(category);
1812
- ruleCountMap.set(ruleId, (ruleCountMap.get(ruleId) ?? 0) + 1);
1832
+ const weight = issue.acknowledged === true ? 0.5 : 1;
1833
+ ruleCountMap.set(ruleId, (ruleCountMap.get(ruleId) ?? 0) + weight);
1813
1834
  }
1814
1835
  for (const category of CATEGORIES) {
1815
1836
  const ruleCountMap = ruleIssueCountPerCategory.get(category);
@@ -1856,7 +1877,8 @@ function calculateScores(result, configs) {
1856
1877
  risk: 0,
1857
1878
  missingInfo: 0,
1858
1879
  suggestion: 0,
1859
- nodeCount
1880
+ nodeCount,
1881
+ acknowledgedCount: 0
1860
1882
  };
1861
1883
  for (const issue of result.issues) {
1862
1884
  switch (issue.config.severity) {
@@ -1873,6 +1895,7 @@ function calculateScores(result, configs) {
1873
1895
  summary.suggestion++;
1874
1896
  break;
1875
1897
  }
1898
+ if (issue.acknowledged === true) summary.acknowledgedCount++;
1876
1899
  }
1877
1900
  return {
1878
1901
  overall: {
@@ -1923,7 +1946,14 @@ function formatScoreSummary(report) {
1923
1946
  lines.push(` Risk: ${report.summary.risk}`);
1924
1947
  lines.push(` Missing Info: ${report.summary.missingInfo}`);
1925
1948
  lines.push(` Suggestion: ${report.summary.suggestion}`);
1926
- lines.push(` Total: ${report.summary.totalIssues}`);
1949
+ if (report.summary.acknowledgedCount > 0) {
1950
+ const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
1951
+ lines.push(
1952
+ ` Total: ${report.summary.totalIssues} (${report.summary.acknowledgedCount} acknowledged via canicode annotations / ${unaddressed} unaddressed)`
1953
+ );
1954
+ } else {
1955
+ lines.push(` Total: ${report.summary.totalIssues}`);
1956
+ }
1927
1957
  return lines.join("\n");
1928
1958
  }
1929
1959
  function buildResultJson(fileName, result, scores, options) {
@@ -1947,17 +1977,20 @@ function buildResultJson(fileName, result, scores, options) {
1947
1977
  ...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
1948
1978
  ...suggestedName !== void 0 ? { suggestedName } : {},
1949
1979
  isInstanceChild: applyContext.isInstanceChild,
1950
- ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
1980
+ ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
1981
+ ...issue.acknowledged === true ? { acknowledged: true } : {}
1951
1982
  };
1952
1983
  });
1953
1984
  const json = {
1954
1985
  version,
1955
1986
  analyzedAt: result.analyzedAt,
1956
1987
  ...options?.fileKey && { fileKey: options.fileKey },
1988
+ ...options?.designKey && { designKey: options.designKey },
1957
1989
  fileName,
1958
1990
  nodeCount: result.nodeCount,
1959
1991
  maxDepth: result.maxDepth,
1960
1992
  issueCount: result.issues.length,
1993
+ acknowledgedCount: scores.summary.acknowledgedCount,
1961
1994
  isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
1962
1995
  blockingIssueCount: scores.summary.blocking,
1963
1996
  scores: {
@@ -2084,26 +2117,97 @@ var GOTCHA_QUESTIONS = {
2084
2117
  }
2085
2118
  };
2086
2119
 
2120
+ // src/core/gotcha/group-and-batch-questions.ts
2121
+ var BATCHABLE_RULE_IDS = [
2122
+ "missing-size-constraint",
2123
+ "irregular-spacing",
2124
+ "no-auto-layout",
2125
+ "fixed-size-in-auto-layout"
2126
+ ];
2127
+ var BATCHABLE_SET = new Set(BATCHABLE_RULE_IDS);
2128
+ var NO_SOURCE_SENTINEL = "_no-source";
2129
+ function groupAndBatchSurveyQuestions(questions) {
2130
+ if (questions.length === 0) {
2131
+ return { groups: [] };
2132
+ }
2133
+ const sorted = [...questions].sort(compareQuestions);
2134
+ const groups = [];
2135
+ let currentGroup = null;
2136
+ let lastGroupKey = null;
2137
+ for (const question of sorted) {
2138
+ const groupKey = sourceComponentKey(question);
2139
+ if (currentGroup === null || groupKey !== lastGroupKey) {
2140
+ currentGroup = {
2141
+ instanceContext: question.instanceContext ?? null,
2142
+ batches: []
2143
+ };
2144
+ groups.push(currentGroup);
2145
+ lastGroupKey = groupKey;
2146
+ }
2147
+ pushIntoBatch(currentGroup, question);
2148
+ }
2149
+ return { groups };
2150
+ }
2151
+ function compareQuestions(a, b) {
2152
+ const aKey = sourceComponentKey(a);
2153
+ const bKey = sourceComponentKey(b);
2154
+ if (aKey !== bKey) {
2155
+ if (aKey === NO_SOURCE_SENTINEL) return 1;
2156
+ if (bKey === NO_SOURCE_SENTINEL) return -1;
2157
+ return aKey.localeCompare(bKey);
2158
+ }
2159
+ if (a.ruleId !== b.ruleId) return a.ruleId.localeCompare(b.ruleId);
2160
+ if (a.nodeName !== b.nodeName) return a.nodeName.localeCompare(b.nodeName);
2161
+ return a.nodeId.localeCompare(b.nodeId);
2162
+ }
2163
+ function sourceComponentKey(question) {
2164
+ return question.instanceContext?.sourceComponentId ?? NO_SOURCE_SENTINEL;
2165
+ }
2166
+ function pushIntoBatch(group, question) {
2167
+ const sceneWeight = Math.max(question.replicas ?? 1, 1);
2168
+ const isBatchable = BATCHABLE_SET.has(question.ruleId);
2169
+ const last = group.batches.at(-1);
2170
+ if (last !== void 0 && last.ruleId === question.ruleId && isBatchable && last.batchable) {
2171
+ last.questions.push(question);
2172
+ last.totalScenes += sceneWeight;
2173
+ return;
2174
+ }
2175
+ group.batches.push({
2176
+ ruleId: question.ruleId,
2177
+ batchable: isBatchable,
2178
+ questions: [question],
2179
+ totalScenes: sceneWeight
2180
+ });
2181
+ }
2182
+
2087
2183
  // src/core/gotcha/survey-generator.ts
2088
2184
  var NODE_PATH_SEPARATOR = " > ";
2089
- function generateGotchaSurvey(result, scores) {
2185
+ function generateGotchaSurvey(result, scores, options = {}) {
2090
2186
  const grade = scores.overall.grade;
2091
2187
  const relevantIssues = result.issues.filter(
2092
2188
  (issue) => issue.config.severity === "blocking" || issue.config.severity === "risk"
2093
2189
  );
2094
2190
  const deduped = deduplicateSiblingIssues(relevantIssues);
2095
2191
  const sorted = stableSortBySeverity(deduped);
2096
- const questions = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
2192
+ const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
2193
+ const questions = deduplicateBySourceComponent(mapped);
2194
+ const groupedQuestions = groupAndBatchSurveyQuestions(questions);
2097
2195
  return {
2098
2196
  designGrade: grade,
2099
2197
  isReadyForCodeGen: isReadyForCodeGen(grade),
2100
- questions
2198
+ questions,
2199
+ groupedQuestions,
2200
+ designKey: options.designKey ?? ""
2101
2201
  };
2102
2202
  }
2103
2203
  function deduplicateSiblingIssues(issues) {
2104
2204
  const seen = /* @__PURE__ */ new Set();
2105
2205
  const result = [];
2106
2206
  for (const issue of issues) {
2207
+ if (isInstanceChildNodeId(issue.violation.nodeId)) {
2208
+ result.push(issue);
2209
+ continue;
2210
+ }
2107
2211
  const parentPath = getParentPath(issue.violation.nodePath);
2108
2212
  const key = `${parentPath}||${issue.violation.ruleId}`;
2109
2213
  if (!seen.has(key)) {
@@ -2163,6 +2267,50 @@ function mapToQuestion(issue, file) {
2163
2267
  ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
2164
2268
  };
2165
2269
  }
2270
+ function deduplicateBySourceComponent(questions) {
2271
+ const groups = /* @__PURE__ */ new Map();
2272
+ const order = [];
2273
+ let uniqueCounter = 0;
2274
+ for (const q of questions) {
2275
+ const ic = q.instanceContext;
2276
+ let key;
2277
+ if (ic && ic.sourceComponentId && ic.sourceNodeId) {
2278
+ key = `${ic.sourceComponentId}::${ic.sourceNodeId}::${q.ruleId}`;
2279
+ } else {
2280
+ key = `__unique__${uniqueCounter++}`;
2281
+ }
2282
+ const bucket = groups.get(key);
2283
+ if (bucket) {
2284
+ bucket.push(q);
2285
+ } else {
2286
+ groups.set(key, [q]);
2287
+ order.push(key);
2288
+ }
2289
+ }
2290
+ return order.map((key) => {
2291
+ const group = groups.get(key);
2292
+ const first = group[0];
2293
+ if (group.length === 1) return first;
2294
+ const otherIds = group.slice(1).map((q) => q.nodeId);
2295
+ const sourceComponentName = first.instanceContext?.sourceComponentName;
2296
+ const template = GOTCHA_QUESTIONS[first.ruleId];
2297
+ const renamed = {
2298
+ ...first,
2299
+ replicas: group.length,
2300
+ replicaNodeIds: otherIds
2301
+ };
2302
+ if (sourceComponentName) {
2303
+ renamed.nodeName = sourceComponentName;
2304
+ if (template) {
2305
+ renamed.question = template.question.replace(
2306
+ "{nodeName}",
2307
+ sourceComponentName
2308
+ );
2309
+ }
2310
+ }
2311
+ return renamed;
2312
+ });
2313
+ }
2166
2314
  function buildInstanceContext(nodeId, file) {
2167
2315
  const parts = parseInstanceChildNodeId(nodeId);
2168
2316
  if (!parts) return null;
@@ -3494,6 +3642,18 @@ function shutdownMonitoring() {
3494
3642
  // src/core/monitoring/keys.ts
3495
3643
  var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
3496
3644
  var SENTRY_DSN = "https://80a836a8300b25f17ef5bbf23afb5b3a@o4511080656207872.ingest.us.sentry.io/4511080661319680" ;
3645
+ function isFigmaUrl2(input) {
3646
+ return input.includes("figma.com/");
3647
+ }
3648
+ z.string();
3649
+ function computeDesignKey(input) {
3650
+ if (isFigmaUrl2(input)) {
3651
+ const { fileKey, nodeId } = parseFigmaUrl(input);
3652
+ if (!nodeId) return fileKey;
3653
+ return `${fileKey}#${nodeId.replace(/-/g, ":")}`;
3654
+ }
3655
+ return resolve(input);
3656
+ }
3497
3657
 
3498
3658
  // src/core/rules/node-semantics.ts
3499
3659
  function isContainerNode(node) {
@@ -4600,6 +4760,7 @@ var inconsistentNamingConventionCheck = (node, context) => {
4600
4760
  if (nodeConvention && nodeConvention !== dominantConvention && maxCount >= 2) {
4601
4761
  if (isCompatible(nodeConvention, dominantConvention, node.name)) return null;
4602
4762
  const suggested = convertName(node.name, dominantConvention);
4763
+ if (suggested === node.name) return null;
4603
4764
  return {
4604
4765
  ruleId: inconsistentNamingConventionDef.id,
4605
4766
  nodeId: node.id,
@@ -4695,6 +4856,15 @@ function hasStateInComponentMaster(node, context, statePattern) {
4695
4856
  if (!master) return false;
4696
4857
  return hasStateInVariantProps(master, statePattern);
4697
4858
  }
4859
+ function canDetermineVariants(node, context) {
4860
+ if (node.type === "COMPONENT") return true;
4861
+ if (node.componentPropertyDefinitions !== void 0) return true;
4862
+ if (node.componentId !== void 0) {
4863
+ const defs = context.file.componentDefinitions;
4864
+ if (defs && defs[node.componentId] !== void 0) return true;
4865
+ }
4866
+ return false;
4867
+ }
4698
4868
  var missingInteractionStateDef = {
4699
4869
  id: "missing-interaction-state",
4700
4870
  name: "Missing Interaction State",
@@ -4709,6 +4879,7 @@ var missingInteractionStateCheck = (node, context) => {
4709
4879
  if (!interactiveType) return null;
4710
4880
  const expectedStates = EXPECTED_STATES[interactiveType];
4711
4881
  if (!expectedStates) return null;
4882
+ if (!canDetermineVariants(node, context)) return null;
4712
4883
  const seen = getSeen(context);
4713
4884
  const nodePath = context.path.join(" > ");
4714
4885
  for (const state of expectedStates) {
@@ -4827,7 +4998,9 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4827
4998
  token: z.string().optional().describe("Figma API token (falls back to FIGMA_TOKEN env var)"),
4828
4999
  preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
4829
5000
  targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
4830
- configPath: z.string().optional().describe("Path to config JSON file for rule overrides")
5001
+ configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
5002
+ openReport: z.boolean().optional().describe("Open the generated HTML report in the user's browser. Defaults to false \u2014 opt in only when a visible report is the explicit user request (#365). The HTML file is always written to disk regardless."),
5003
+ acknowledgments: z.array(AcknowledgmentSchema).optional().describe("(#371) Pre-resolved [{ nodeId, ruleId }] pairs harvested from canicode-authored Figma annotations (e.g. via the `readCanicodeAcknowledgments` Plugin helper inside a use_figma batch). Matching issues are flagged `acknowledged: true` and contribute half weight to the density score so re-analyze surfaces movement after a roundtrip even under ADR-012's annotate-by-default policy.")
4831
5004
  },
4832
5005
  {
4833
5006
  readOnlyHint: false,
@@ -4835,7 +5008,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4835
5008
  openWorldHint: true,
4836
5009
  title: "Analyze Figma Design"
4837
5010
  },
4838
- async ({ input, token, preset, targetNodeId, configPath }) => {
5011
+ async ({ input, token, preset, targetNodeId, configPath, openReport, acknowledgments }) => {
4839
5012
  trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
4840
5013
  try {
4841
5014
  const { file, nodeId } = await loadFile(input, token);
@@ -4847,7 +5020,8 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4847
5020
  }
4848
5021
  const result = analyzeFile(file, {
4849
5022
  configs,
4850
- ...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
5023
+ ...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {},
5024
+ ...acknowledgments && acknowledgments.length > 0 ? { acknowledgments } : {}
4851
5025
  });
4852
5026
  const scores = calculateScores(result, configs);
4853
5027
  const figmaToken = token ?? process.env["FIGMA_TOKEN"];
@@ -4856,14 +5030,16 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4856
5030
  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")}`;
4857
5031
  ensureReportsDir();
4858
5032
  const reportPath = `${getReportsDir()}/report-${ts}-${file.fileKey}.html`;
4859
- await new Promise((resolve5, reject) => {
5033
+ await new Promise((resolve6, reject) => {
4860
5034
  writeFile(reportPath, html, "utf-8", (err) => {
4861
5035
  if (err) reject(err);
4862
- else resolve5();
5036
+ else resolve6();
4863
5037
  });
4864
5038
  });
4865
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
4866
- exec(`${openCmd} "${reportPath}"`);
5039
+ if (openReport === true) {
5040
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5041
+ exec(`${openCmd} "${reportPath}"`);
5042
+ }
4867
5043
  trackEvent(EVENTS.ANALYSIS_COMPLETED, {
4868
5044
  nodeCount: result.nodeCount,
4869
5045
  issueCount: result.issues.length,
@@ -4875,7 +5051,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4875
5051
  content: [
4876
5052
  {
4877
5053
  type: "text",
4878
- text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey }), null, 2)
5054
+ text: JSON.stringify(buildResultJson(file.name, result, scores, { fileKey: file.fileKey, designKey: computeDesignKey(input) }), null, 2)
4879
5055
  }
4880
5056
  ]
4881
5057
  };
@@ -4935,7 +5111,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
4935
5111
  ...effectiveNodeId ? { targetNodeId: effectiveNodeId } : {}
4936
5112
  });
4937
5113
  const scores = calculateScores(result, configs);
4938
- const survey = generateGotchaSurvey(result, scores);
5114
+ const survey = generateGotchaSurvey(result, scores, { designKey: computeDesignKey(input) });
4939
5115
  trackEvent(EVENTS.ANALYSIS_COMPLETED, {
4940
5116
  nodeCount: result.nodeCount,
4941
5117
  issueCount: result.issues.length,
@@ -5056,10 +5232,10 @@ Get your token: Figma \u2192 Settings \u2192 Security \u2192 Personal access tok
5056
5232
 
5057
5233
  ## MCP Server (Claude Code / Cursor / Claude Desktop)
5058
5234
  \`\`\`bash
5059
- claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y -p canicode canicode-mcp
5235
+ claude mcp add canicode -- npx --yes --package=canicode canicode-mcp
5060
5236
  \`\`\`
5061
5237
 
5062
- Requires FIGMA_TOKEN for live Figma URL analysis.
5238
+ Requires FIGMA_TOKEN for live Figma URL analysis. The MCP server reads it from \`~/.canicode/config.json\` (set via \`canicode init --token \u2026\`) or from the host's environment, so do **not** pass \`-e FIGMA_TOKEN=\u2026\` to \`claude mcp add\` \u2014 \`@anthropic-ai/claude-code\`'s current parser rejects short-form flags placed before \`--\`. (#364, #366)
5063
5239
 
5064
5240
  ## CLI only (no MCP server)
5065
5241
 
@@ -5179,16 +5355,16 @@ ${inlineTopics[selectedTopic]}` }]
5179
5355
  };
5180
5356
  }
5181
5357
  const { readFile: readFile3 } = await import('fs/promises');
5182
- const { resolve: resolve5, dirname: dirname4 } = await import('path');
5358
+ const { resolve: resolve6, dirname: dirname4 } = await import('path');
5183
5359
  const { fileURLToPath } = await import('url');
5184
5360
  try {
5185
5361
  const __dirname = dirname4(fileURLToPath(import.meta.url));
5186
- const docPath = resolve5(__dirname, "../../docs/CUSTOMIZATION.md");
5362
+ const docPath = resolve6(__dirname, "../../docs/CUSTOMIZATION.md");
5187
5363
  let content;
5188
5364
  try {
5189
5365
  content = await readFile3(docPath, "utf-8");
5190
5366
  } catch {
5191
- const altPath = resolve5(__dirname, "../docs/CUSTOMIZATION.md");
5367
+ const altPath = resolve6(__dirname, "../docs/CUSTOMIZATION.md");
5192
5368
  content = await readFile3(altPath, "utf-8");
5193
5369
  }
5194
5370
  if (selectedTopic !== "all") {