@shortwind/cli 0.1.0-beta.11 → 0.1.0-beta.13

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.
@@ -5,11 +5,11 @@ import { fileURLToPath } from "node:url";
5
5
  import { applyEdits, modify, parse } from "jsonc-parser";
6
6
  import { PLACEHOLDER_SHA, RECIPE_SHA_HEX_LENGTH, buildRegistry, isReservedRecipeName, normalizeRecipeBody, parseRecipeFile, renderSkillMarkdown } from "@shortwind/core";
7
7
  import { createHash } from "node:crypto";
8
+ import { findTailwindEntryCssFiles, loadRegistryFromDir, modeForFile, syncSourceDirectiveToFile, transformContent } from "@shortwind/tailwind";
8
9
  import { glob } from "tinyglobby";
9
10
  import chokidar from "chokidar";
10
11
  import { Tiktoken } from "js-tiktoken/lite";
11
12
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
12
- import { loadRegistryFromDir, modeForFile, transformContent } from "@shortwind/tailwind";
13
13
  //#region src/fingerprint.ts
14
14
  const HEADER_PATTERN = /^\/\*\s*shortwind:\s+(\S+)@(\S+)\s+sha:([^\s*]+)(?:\s+—\s+DO NOT EDIT THIS LINE)?\s*\*\/\s*$/;
15
15
  function extractHeader(source) {
@@ -58,6 +58,9 @@ function sealRecipeFile(source, family, version) {
58
58
  }
59
59
  //#endregion
60
60
  //#region src/detect.ts
61
+ function skillAdapterFor(bundler) {
62
+ return bundler === "unknown" ? void 0 : bundler;
63
+ }
61
64
  function parsePackageJson(pkgPath) {
62
65
  let parsed;
63
66
  try {
@@ -165,7 +168,7 @@ const BUNDLED_ORIGIN = "bundled:@shortwind/catalog";
165
168
  function bundledSource() {
166
169
  let cache = null;
167
170
  const load = () => {
168
- cache ??= import("./catalog.generated-DRttebfK.js");
171
+ cache ??= import("./catalog.generated-BdZstlkf.js");
169
172
  return cache;
170
173
  };
171
174
  return {
@@ -203,9 +206,26 @@ function fileSource(origin) {
203
206
  }
204
207
  };
205
208
  }
209
+ const FETCH_RETRIES = 2;
210
+ const RETRY_BASE_DELAY_MS = 500;
211
+ function isTransientFetchError(err) {
212
+ return err instanceof DOMException && err.name === "TimeoutError" || err instanceof TypeError;
213
+ }
206
214
  function httpSource(origin) {
207
215
  const base = origin.replace(/\/+$/, "");
208
- const get = (url) => fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
216
+ const get = async (url) => {
217
+ let lastErr;
218
+ for (let attempt = 0; attempt <= FETCH_RETRIES; attempt++) {
219
+ if (attempt > 0) await new Promise((r) => setTimeout(r, RETRY_BASE_DELAY_MS * attempt));
220
+ try {
221
+ return await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
222
+ } catch (err) {
223
+ if (!isTransientFetchError(err)) throw err;
224
+ lastErr = err;
225
+ }
226
+ }
227
+ throw lastErr;
228
+ };
209
229
  let presetsCache = null;
210
230
  return {
211
231
  origin,
@@ -416,6 +436,34 @@ async function scaffoldTheme(cwd) {
416
436
  };
417
437
  }
418
438
  const THEME_COLOR_TOKENS = new Set([...THEME_BLOCK.matchAll(/--color-([\w-]+)\s*:/g)].map((m) => m[1] ?? ""));
439
+ const THEME_SUPPLEMENT_MARKER = "/* shortwind:theme-supplement";
440
+ function blockSectionValues(selector) {
441
+ const m = THEME_BLOCK.match(new RegExp(`${selector.replace(".", "\\.")}\\s*\\{([^}]*)\\}`));
442
+ const out = /* @__PURE__ */ new Map();
443
+ for (const decl of (m?.[1] ?? "").matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)) out.set(decl[1], decl[2].trim());
444
+ return out;
445
+ }
446
+ const THEME_LIGHT_VALUES = blockSectionValues(":root");
447
+ const THEME_DARK_VALUES = blockSectionValues(".dark");
448
+ function buildThemeSupplement(css, missing) {
449
+ const tokens = missing.filter((t) => THEME_LIGHT_VALUES.has(t));
450
+ if (tokens.length === 0) return null;
451
+ const light = tokens.map((t) => ` --${t}: ${THEME_LIGHT_VALUES.get(t)};`);
452
+ const dark = tokens.filter((t) => THEME_DARK_VALUES.has(t)).map((t) => ` --${t}: ${THEME_DARK_VALUES.get(t)};`);
453
+ const mapping = tokens.map((t) => ` --color-${t}: var(--${t});`);
454
+ const lines = [
455
+ `${THEME_SUPPLEMENT_MARKER} — placeholder values for tokens your theme didn't define. Tune them to your palette. */`,
456
+ ":root {",
457
+ ...light,
458
+ "}"
459
+ ];
460
+ if (dark.length > 0) {
461
+ if (/@custom-variant\s+dark|\.dark\s*\{/.test(css)) lines.push(".dark {", ...dark, "}");
462
+ else if (/@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)/.test(css)) lines.push("@media (prefers-color-scheme: dark) {", " :root {", ...dark.map((l) => ` ${l}`), " }", "}");
463
+ }
464
+ lines.push("@theme inline {", ...mapping, "}", "/* end shortwind theme-supplement */");
465
+ return lines.join("\n");
466
+ }
419
467
  const COLOR_UTILITY_RE = /^(?:bg|text|border|ring|outline|fill|stroke|divide|accent|caret|decoration|shadow|from|via|to|placeholder)-(.+)$/;
420
468
  function referencedThemeTokens(flattened) {
421
469
  const out = /* @__PURE__ */ new Set();
@@ -452,21 +500,22 @@ const VITE_CONFIGS = [
452
500
  const VITE_SNIPPET = [
453
501
  `import { shortwind } from "@shortwind/vite";`,
454
502
  `// add shortwind() to the Vite plugins array — it runs in the pre phase,`,
455
- `// before Tailwind's scan:`,
456
- `// plugins: [shortwind(), tailwindcss(), react()]`
503
+ `// before Tailwind's scan. strict: true fails the build when a recipe`,
504
+ `// token leaks unexpanded (recommended; drop it to demote to a warning):`,
505
+ `// plugins: [shortwind({ strict: true }), tailwindcss(), react()]`
457
506
  ].join("\n");
458
507
  async function wireBundler(cwd, bundler) {
459
508
  if (bundler === "vite") return wireVite(cwd);
460
509
  if (bundler === "next") return {
461
510
  configPath: null,
462
511
  action: "manual",
463
- snippet: `import { withShortwind } from "@shortwind/next";\n// withShortwind is curried — wrap your Next config:\n// export default withShortwind()(nextConfig);`,
512
+ snippet: `import { withShortwind } from "@shortwind/next";\n// withShortwind is curried — wrap your Next config. strict: true fails\n// the build when a recipe token leaks unexpanded (recommended):\n// export default withShortwind({ strict: true })(nextConfig);`,
464
513
  reason: "Next config wiring is manual"
465
514
  };
466
515
  if (bundler === "astro") return {
467
516
  configPath: null,
468
517
  action: "manual",
469
- snippet: `import shortwind from "@shortwind/astro";\n// add to integrations: integrations: [shortwind()]`,
518
+ snippet: `import shortwind from "@shortwind/astro";\n// add to integrations — strict: true fails the build when a recipe\n// token leaks unexpanded (recommended):\n// integrations: [shortwind({ strict: true })]`,
470
519
  reason: "Astro config wiring is manual"
471
520
  };
472
521
  return {
@@ -514,25 +563,33 @@ function addImport(source, line) {
514
563
  //#endregion
515
564
  //#region src/agents-file.ts
516
565
  const MARKER = "skills/shortwind/SKILL.md";
566
+ const DYNAMIC_MARKER = "expandClassList";
517
567
  function line(skillRel) {
518
568
  return `For UI, prefer Shortwind \`@recipe\` class names (e.g. \`@card\`, \`@btn-primary\`, \`@row\`) over raw Tailwind where a recipe fits — full catalog in \`${skillRel}\`.`;
519
569
  }
570
+ function dynamicLine() {
571
+ return "Never build a recipe name dynamically (variable, prop, concatenation) — it silently won't expand. For a runtime choice between recipes use the `rc()`/`expandClassList` helper, and turn on `strict: true` in the Shortwind adapter config to fail the build on leaked `@tokens` — worked snippets under \"Dynamic classes\" in the SKILL doc above.";
572
+ }
520
573
  const CANDIDATES = ["AGENTS.md", "CLAUDE.md"];
521
574
  async function wireAgentsInstructions(cwd, skillPath) {
522
575
  const pointer = line(path.relative(cwd, skillPath).split(path.sep).join("/"));
576
+ const dynamic = dynamicLine();
523
577
  let touched = null;
524
578
  for (const name of CANDIDATES) {
525
579
  const file = path.join(cwd, name);
526
580
  if (!existsSync(file)) continue;
527
581
  const current = await readFile(file, "utf8");
528
- if (current.includes(MARKER)) {
582
+ const missing = [];
583
+ if (!current.includes(MARKER)) missing.push(pointer);
584
+ if (!current.includes(DYNAMIC_MARKER)) missing.push(dynamic);
585
+ if (missing.length === 0) {
529
586
  touched ??= {
530
587
  path: file,
531
588
  action: "skipped"
532
589
  };
533
590
  continue;
534
591
  }
535
- await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") + pointer + "\n");
592
+ await writeFile(file, current + (current.endsWith("\n\n") ? "" : current.endsWith("\n") ? "\n" : "\n\n") + missing.join("\n") + "\n");
536
593
  return {
537
594
  path: file,
538
595
  action: "appended"
@@ -540,7 +597,7 @@ async function wireAgentsInstructions(cwd, skillPath) {
540
597
  }
541
598
  if (touched) return touched;
542
599
  const target = path.join(cwd, "AGENTS.md");
543
- await writeFile(target, `# AGENTS.md\n\n${pointer}\n`);
600
+ await writeFile(target, `# AGENTS.md\n\n${pointer}\n${dynamic}\n`);
544
601
  return {
545
602
  path: target,
546
603
  action: "created"
@@ -568,7 +625,15 @@ async function init(options) {
568
625
  installError = err instanceof Error ? err.message : String(err);
569
626
  }
570
627
  const recipesDir = path.join(cwd, "recipes");
571
- const { installed, skipped } = await copyRecipes(source, families, recipesDir);
628
+ let copied;
629
+ try {
630
+ copied = await copyRecipes(source, families, recipesDir);
631
+ } catch (err) {
632
+ const done = families.filter((f) => existsSync(path.join(recipesDir, `${f}.css`)));
633
+ const reason = err instanceof Error ? err.message : String(err);
634
+ throw new Error(`init aborted while copying recipe families (${done.length}/${families.length} copied${done.length > 0 ? `: ${done.join(", ")}` : ""}) — ${reason}\nThe project is incomplete (no config/SKILL.md/theme yet). Re-run the same init command to resume; already-copied families are skipped.`);
635
+ }
636
+ const { installed, skipped } = copied;
572
637
  await updateLockfile(recipesDir, registry, installed);
573
638
  const configPath = path.join(cwd, "shortwind.config.json");
574
639
  await writeConfig(configPath, {
@@ -577,17 +642,34 @@ async function init(options) {
577
642
  });
578
643
  const vscodePath = path.join(cwd, ".vscode", "settings.json");
579
644
  await wireVscodeClassRegex(vscodePath);
580
- const huskyPath = path.join(cwd, ".husky", "pre-commit");
581
- await installHuskyHook(huskyPath);
645
+ const huskyPath = await installHuskyHook(cwd, path.join(cwd, ".husky", "pre-commit"));
582
646
  const skillPath = path.join(cwd, "skills", "shortwind", "SKILL.md");
583
- const skillRegistry = await writeSkillMd(skillPath, recipesDir, families);
647
+ const skillRegistry = await writeSkillMd(skillPath, recipesDir, families, shape.bundler);
584
648
  const theme = await scaffoldTheme(cwd);
649
+ let themeAction = theme.action;
585
650
  let missingThemeTokens = [];
586
- if (theme.action === "skipped" && theme.themePath && skillRegistry) missingThemeTokens = findMissingThemeTokens(await readFile(theme.themePath, "utf8"), skillRegistry.flattened);
651
+ let supplementedThemeTokens = [];
652
+ if (theme.action === "skipped" && theme.themePath && skillRegistry) {
653
+ const css = await readFile(theme.themePath, "utf8");
654
+ missingThemeTokens = findMissingThemeTokens(css, skillRegistry.flattened);
655
+ const supplement = buildThemeSupplement(css, missingThemeTokens);
656
+ if (supplement) {
657
+ await writeFile(theme.themePath, `${css.replace(/\s*$/, "")}\n\n${supplement}\n`);
658
+ supplementedThemeTokens = missingThemeTokens;
659
+ missingThemeTokens = [];
660
+ themeAction = "supplemented";
661
+ }
662
+ }
663
+ const safelistCssPaths = [];
664
+ if (shape.bundler !== "vite" && shape.bundler !== "astro" && skillRegistry) for (const file of findTailwindEntryCssFiles(cwd)) {
665
+ syncSourceDirectiveToFile(file, skillRegistry);
666
+ safelistCssPaths.push(file);
667
+ }
587
668
  const bundlerConfig = await wireBundler(cwd, shape.bundler);
588
669
  const agentsFile = await wireAgentsInstructions(cwd, skillPath);
589
670
  return {
590
671
  packageManager: shape.packageManager,
672
+ bundler: shape.bundler,
591
673
  preset: options.preset,
592
674
  registry,
593
675
  families,
@@ -599,7 +681,9 @@ async function init(options) {
599
681
  huskyPath,
600
682
  skillPath,
601
683
  themePath: theme.themePath,
602
- themeAction: theme.action,
684
+ themeAction,
685
+ supplementedThemeTokens,
686
+ safelistCssPaths,
603
687
  missingThemeTokens,
604
688
  bundlerConfigPath: bundlerConfig.configPath,
605
689
  bundlerConfigAction: bundlerConfig.action,
@@ -739,18 +823,20 @@ async function wireVscodeClassRegex(vscodePath) {
739
823
  parse(next);
740
824
  await writeFile(vscodePath, next.endsWith("\n") ? next : next + "\n");
741
825
  }
742
- const HUSKY_LINE = "npx shortwind build";
743
- async function installHuskyHook(huskyPath) {
826
+ const HUSKY_LINE = "npx @shortwind/cli build";
827
+ async function installHuskyHook(cwd, huskyPath) {
828
+ if (!existsSync(path.join(cwd, ".git"))) return null;
744
829
  await mkdir(path.dirname(huskyPath), { recursive: true });
745
830
  if (!existsSync(huskyPath)) {
746
831
  await writeFile(huskyPath, `${HUSKY_LINE}\n`, { mode: 493 });
747
- return;
832
+ return huskyPath;
748
833
  }
749
834
  const current = await readFile(huskyPath, "utf8");
750
- if (current.includes(HUSKY_LINE)) return;
751
- await writeFile(huskyPath, current.endsWith("\n") ? current + HUSKY_LINE + "\n" : current + "\nnpx shortwind build\n", { mode: 493 });
835
+ if (current.includes(HUSKY_LINE)) return huskyPath;
836
+ await writeFile(huskyPath, current.endsWith("\n") ? current + HUSKY_LINE + "\n" : current + "\nnpx @shortwind/cli build\n", { mode: 493 });
837
+ return huskyPath;
752
838
  }
753
- async function writeSkillMd(skillPath, recipesDir, families) {
839
+ async function writeSkillMd(skillPath, recipesDir, families, bundler) {
754
840
  const allRecipes = [];
755
841
  const guidance = {};
756
842
  const problems = [];
@@ -770,7 +856,11 @@ async function writeSkillMd(skillPath, recipesDir, families) {
770
856
  return null;
771
857
  }
772
858
  await mkdir(path.dirname(skillPath), { recursive: true });
773
- await writeFile(skillPath, renderSkillMarkdown(resolved.value, { order: families }));
859
+ const adapter = skillAdapterFor(bundler);
860
+ await writeFile(skillPath, renderSkillMarkdown(resolved.value, {
861
+ order: families,
862
+ ...adapter ? { adapter } : {}
863
+ }));
774
864
  return resolved.value;
775
865
  }
776
866
  //#endregion
@@ -804,11 +894,16 @@ async function readConfig(cwd) {
804
894
  ...DEFAULT_CONFIG,
805
895
  ...parsed
806
896
  };
807
- return {
897
+ const config = {
808
898
  registry: assertConfigString(merged.registry, "registry", configPath),
809
899
  recipesDir: assertWithinCwd(cwd, assertConfigString(merged.recipesDir, "recipesDir", configPath), "recipesDir", configPath),
810
900
  outputPath: assertWithinCwd(cwd, assertConfigString(merged.outputPath, "outputPath", configPath), "outputPath", configPath)
811
901
  };
902
+ if (merged.content !== void 0) {
903
+ if (!Array.isArray(merged.content) || merged.content.some((g) => typeof g !== "string")) throw new Error(`${configPath}: "content" must be an array of glob strings`);
904
+ config.content = merged.content;
905
+ }
906
+ return config;
812
907
  }
813
908
  function installedFamilies(recipesDir) {
814
909
  if (!existsSync(recipesDir)) return [];
@@ -846,7 +941,11 @@ async function regenerateSkillMd(cwd, config) {
846
941
  }
847
942
  const { mkdir } = await import("node:fs/promises");
848
943
  await mkdir(path.dirname(skillPath), { recursive: true });
849
- await writeFile(skillPath, renderSkillMarkdown(resolved.value, { order: families }));
944
+ const adapter = skillAdapterFor(detectProject(cwd).bundler);
945
+ await writeFile(skillPath, renderSkillMarkdown(resolved.value, {
946
+ order: families,
947
+ ...adapter ? { adapter } : {}
948
+ }));
850
949
  return skillPath;
851
950
  }
852
951
  /**
@@ -1164,8 +1263,13 @@ async function build(options) {
1164
1263
  if (errors.length > 0) throw new BuildError(errors);
1165
1264
  const resolved = buildRegistry(allRecipes, { guidance });
1166
1265
  if (!resolved.ok) throw new BuildError(resolved.errors);
1266
+ const { bundler } = detectProject(cwd);
1267
+ const adapter = skillAdapterFor(bundler);
1167
1268
  const skillPath = path.join(cwd, config.outputPath);
1168
- const next = renderSkillMarkdown(resolved.value, { order: families });
1269
+ const next = renderSkillMarkdown(resolved.value, {
1270
+ order: families,
1271
+ ...adapter ? { adapter } : {}
1272
+ });
1169
1273
  const current = existsSync(skillPath) ? readFileSync(skillPath, "utf8") : null;
1170
1274
  let changed = false;
1171
1275
  if (current !== next) {
@@ -1173,10 +1277,16 @@ async function build(options) {
1173
1277
  await writeFile(skillPath, next);
1174
1278
  changed = true;
1175
1279
  }
1280
+ const safelistCssPaths = [];
1281
+ if (bundler !== "vite" && bundler !== "astro") for (const file of findTailwindEntryCssFiles(cwd)) {
1282
+ if (syncSourceDirectiveToFile(file, resolved.value)) changed = true;
1283
+ safelistCssPaths.push(file);
1284
+ }
1176
1285
  return {
1177
1286
  changed,
1178
1287
  families,
1179
- skillPath
1288
+ skillPath,
1289
+ safelistCssPaths
1180
1290
  };
1181
1291
  }
1182
1292
  //#endregion
@@ -1530,7 +1640,7 @@ const DEFAULT_RECIPES_CSS = {
1530
1640
  "dialog.css": "/* shortwind: dialog@0.0.1 sha:000000 */\n\n/* @guide\n A modal is three layers: @dialog-overlay (dimmed backdrop), @dialog (the\n centering wrapper), and @dialog-content (the panel). Structure the panel with\n @dialog-header and @dialog-footer. Don't put content styling on @dialog\n itself — it's only the positioner.\n*/\n\n/* Modal dialog wrapper — covers the viewport, centers content. */\n@recipe dialog {\n fixed inset-0 z-50 flex items-center justify-center p-4\n}\n\n/* Dimmed overlay behind the dialog. */\n@recipe dialog-overlay {\n fixed inset-0 z-40 bg-black/50\n}\n\n/* Dialog content panel. */\n@recipe dialog-content {\n relative z-50 w-full max-w-md rounded-lg border border-border bg-popover text-popover-foreground p-6 shadow-xl\n}\n\n/* Dialog header region with title. */\n@recipe dialog-header {\n mb-4 flex flex-col gap-1\n}\n\n/* Dialog footer with right-aligned actions. */\n@recipe dialog-footer {\n mt-6 flex items-center justify-end gap-2\n}\n",
1531
1641
  "empty.css": "/* shortwind: empty@0.0.1 sha:000000 */\n\n/* @guide\n @empty is the container for a no-data state; fill it with @empty-icon,\n @empty-title, and @empty-description. These are slots for that pattern — use\n @heading-* / @body from the text family for ordinary copy.\n*/\n\n/* Empty-state container. */\n@recipe empty {\n flex flex-col items-center justify-center gap-3 rounded-md border border-dashed border-border p-8 text-center\n}\n\n/* Empty-state icon slot. */\n@recipe empty-icon {\n flex h-12 w-12 items-center justify-center rounded-full bg-muted text-muted-foreground\n}\n\n/* Empty-state title text. */\n@recipe empty-title {\n text-base font-semibold text-foreground\n}\n\n/* Empty-state supporting description. */\n@recipe empty-description {\n max-w-sm text-sm text-muted-foreground\n}\n",
1532
1642
  "feedback.css": "/* shortwind: feedback@0.0.1 sha:000000 */\n\n/* @guide\n Inline messages use @alert (neutral) or a tone variant @alert-success/\n warning/danger/info — one tone each. @callout is a left-accent inline note,\n @toast is a floating notification, @banner spans the full viewport width.\n*/\n\n/* Default informational alert. */\n@recipe alert {\n flex items-start gap-3 rounded-md border border-border bg-card p-4 text-sm text-card-foreground\n}\n\n/* Success alert. */\n@recipe alert-success {\n flex items-start gap-3 rounded-md border border-green-200 bg-green-50 p-4 text-sm text-green-900 dark:border-green-900 dark:bg-green-950 dark:text-green-100\n}\n\n/* Warning alert. */\n@recipe alert-warning {\n flex items-start gap-3 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-100\n}\n\n/* Danger alert. */\n@recipe alert-danger {\n flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive\n}\n\n/* Informational alert. */\n@recipe alert-info {\n flex items-start gap-3 rounded-md border border-primary/30 bg-primary/10 p-4 text-sm text-primary\n}\n\n/* Inline callout — flush left edge accent. */\n@recipe callout {\n border-l-4 border-primary bg-primary/10 p-4 text-sm text-foreground\n}\n\n/* Floating toast notification. */\n@recipe toast {\n pointer-events-auto flex items-start gap-3 rounded-md border border-border bg-popover p-4 text-sm text-popover-foreground shadow-lg\n}\n\n/* Full-width banner spanning the viewport. */\n@recipe banner {\n w-full bg-primary px-4 py-2 text-center text-sm font-medium text-primary-foreground\n}\n",
1533
- "form.css": "/* shortwind: form@0.0.1 sha:000000 */\n\n/* @guide\n Wrap each label+control+message in @field (use @field-error for the invalid\n state); group related fields with @fieldset. Controls are bare: @input,\n @textarea, @select, @checkbox, @radio, plus @input-error for invalid text\n and @input-shell for the transparent shadcn-style shell. Helper text is\n @help. There is no @form-group (use @field), @form-input (use @input),\n @form-helper (use @help) or @form-checkbox (use @checkbox); the field label\n recipe is @label, in the text family.\n*/\n\n/* Text input field. */\n@recipe input {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* shadcn/dinachi-style input shell — transparent background, h-9, file/\n placeholder/selection/aria-invalid/focus-visible states baked in. */\n@recipe input-shell {\n flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\n}\n\n/* Input in error state. */\n@recipe input-error {\n @input border-destructive focus:border-destructive focus:outline-destructive\n}\n\n/* Multi-line textarea. */\n@recipe textarea {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Native select control. */\n@recipe select {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Checkbox input. */\n@recipe checkbox {\n h-4 w-4 rounded border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Radio input. */\n@recipe radio {\n h-4 w-4 border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Form field wrapper — label + input + help/error. */\n@recipe field {\n flex flex-col gap-1.5\n}\n\n/* Form field in error state. */\n@recipe field-error {\n flex flex-col gap-1.5\n}\n\n/* Grouped form section with optional legend. */\n@recipe fieldset {\n flex flex-col gap-4 rounded-md border border-border p-4\n}\n\n/* Field-level helper text. */\n@recipe help {\n text-xs text-muted-foreground\n}\n",
1643
+ "form.css": "/* shortwind: form@0.0.2 sha:000000 */\n\n/* @guide\n Wrap each label+control+message in @field (use @field-error for the invalid\n state); group related fields with @fieldset. Controls are bare: @input,\n @textarea, @select, @checkbox, @radio, plus @input-error for invalid text\n and @input-shell for the transparent input shell. Helper text is\n @help. There is no @form-group (use @field), @form-input (use @input),\n @form-helper (use @help) or @form-checkbox (use @checkbox); the field label\n recipe is @label, in the text family.\n*/\n\n/* Text input field. */\n@recipe input {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Dinachi-style input shell — transparent background, h-9, file/\n placeholder/selection/aria-invalid/focus-visible states baked in. */\n@recipe input-shell {\n flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40\n}\n\n/* Input in error state. */\n@recipe input-error {\n @input border-destructive focus:border-destructive focus:outline-destructive\n}\n\n/* Multi-line textarea. */\n@recipe textarea {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Native select control. */\n@recipe select {\n block w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:border-ring focus:outline-2 focus:outline-offset-2 focus:outline-ring disabled:cursor-not-allowed disabled:opacity-50\n}\n\n/* Checkbox input. */\n@recipe checkbox {\n h-4 w-4 rounded border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Radio input. */\n@recipe radio {\n h-4 w-4 border-input text-primary focus:outline-2 focus:outline-offset-2 focus:outline-ring\n}\n\n/* Form field wrapper — label + input + help/error. */\n@recipe field {\n flex flex-col gap-1.5\n}\n\n/* Form field in error state. */\n@recipe field-error {\n flex flex-col gap-1.5\n}\n\n/* Grouped form section with optional legend. */\n@recipe fieldset {\n flex flex-col gap-4 rounded-md border border-border p-4\n}\n\n/* Field-level helper text. */\n@recipe help {\n text-xs text-muted-foreground\n}\n",
1534
1644
  "icon.css": "/* shortwind: icon@0.0.1 sha:000000 */\n\n/* @guide\n Size an icon with @icon-sm/md/lg (16/20/24px) — these set width and height\n only; add @icon-muted for secondary color. They're for SVG/icon elements, not\n to be confused with @btn-icon (the icon button in the button family).\n*/\n\n/* Small icon — 16px. */\n@recipe icon-sm {\n h-4 w-4 shrink-0\n}\n\n/* Default icon size — 20px. */\n@recipe icon-md {\n h-5 w-5 shrink-0\n}\n\n/* Large icon — 24px. */\n@recipe icon-lg {\n h-6 w-6 shrink-0\n}\n\n/* Icon with muted color. */\n@recipe icon-muted {\n text-muted-foreground\n}\n",
1535
1645
  "layout.css": "/* shortwind: layout@0.0.1 sha:000000 */\n\n/* @guide\n Composition primitives. @stack-* stacks children vertically (flex-col);\n @row* lays them out horizontally (flex-row). Choose the gap with the size\n suffix (xs/sm/md/lg on stacks). Use @grid-2/3/4 only for true multi-column\n grids, @center to center on both axes, @full to fill the parent. Common\n slips: there is no @flex-row (use @row) or @flex-col (use a @stack-*), and\n the grids are @grid-3, not @grid-cols-3.\n*/\n\n/* Vertical stack with extra-small gap. */\n@recipe stack-xs {\n flex flex-col gap-1\n}\n\n/* Vertical stack with small gap. */\n@recipe stack-sm {\n flex flex-col gap-2\n}\n\n/* Vertical stack with medium gap. */\n@recipe stack-md {\n flex flex-col gap-4\n}\n\n/* Vertical stack with large gap. */\n@recipe stack-lg {\n flex flex-col gap-8\n}\n\n/* Horizontal row with default gap and centered items. */\n@recipe row {\n flex flex-row items-center gap-2\n}\n\n/* Horizontal row with space between children. */\n@recipe row-between {\n flex flex-row items-center justify-between gap-2\n}\n\n/* Horizontal row aligned to the end. */\n@recipe row-end {\n flex flex-row items-center justify-end gap-2\n}\n\n/* Two-column responsive grid. */\n@recipe grid-2 {\n grid grid-cols-1 gap-4 sm:grid-cols-2\n}\n\n/* Three-column responsive grid. */\n@recipe grid-3 {\n grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\n}\n\n/* Four-column responsive grid. */\n@recipe grid-4 {\n grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\n}\n\n/* Center content horizontally and vertically. */\n@recipe center {\n flex items-center justify-center\n}\n\n/* Fill the available width and height. */\n@recipe full {\n h-full w-full\n}\n",
1536
1646
  "list.css": "/* shortwind: list@0.0.1 sha:000000 */\n\n/* @guide\n @list wraps a stack of @list-item rows; use @list-bordered for divided rows.\n Definition lists are separate: @dl with @dt (term) and @dd (description).\n For site navigation reach for the navigation family (@nav), not @list.\n*/\n\n/* Vertical list with default gap. */\n@recipe list {\n flex flex-col gap-1\n}\n\n/* Single list item. */\n@recipe list-item {\n flex items-center gap-2 rounded-md px-3 py-2 text-sm text-foreground\n}\n\n/* List with dividing borders between items. */\n@recipe list-bordered {\n divide-y divide-border rounded-md border border-border\n}\n\n/* Definition list container. */\n@recipe dl {\n grid grid-cols-1 gap-2 sm:grid-cols-3 sm:gap-4\n}\n\n/* Definition term. */\n@recipe dt {\n text-sm font-medium text-muted-foreground\n}\n\n/* Definition description. */\n@recipe dd {\n text-sm text-foreground sm:col-span-2\n}\n",
@@ -1538,9 +1648,9 @@ const DEFAULT_RECIPES_CSS = {
1538
1648
  "navigation.css": "/* shortwind: navigation@0.0.1 sha:000000 */\n\n/* @guide\n @nav is the container; links are @nav-link with @nav-link-active for the\n current page. Tabs mirror that pair: @tab and @tab-active. Use @breadcrumb\n for trail navigation. Active and inactive are separate recipes — swap the\n whole class rather than combining them.\n*/\n\n/* Top-level nav container. */\n@recipe nav {\n flex items-center gap-1\n}\n\n/* Inactive nav link with hover/focus states. */\n@recipe nav-link {\n inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Active nav link. */\n@recipe nav-link-active {\n inline-flex items-center gap-2 rounded-md bg-muted px-3 py-1.5 text-sm font-medium text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Breadcrumb trail container. */\n@recipe breadcrumb {\n flex items-center gap-1.5 text-sm text-muted-foreground\n}\n\n/* Inactive tab control. */\n@recipe tab {\n inline-flex items-center gap-2 border-b-2 border-transparent px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:border-border hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n\n/* Active tab control. */\n@recipe tab-active {\n inline-flex items-center gap-2 border-b-2 border-primary px-3 py-2 text-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n",
1539
1649
  "progress.css": "/* shortwind: progress@0.0.1 sha:000000 */\n\n/* @guide\n A bar is two pieces: @progress-track (the background) wrapping @progress-bar\n (the fill). For an indeterminate state use @spinner instead — it's a\n standalone loader, not a bar.\n*/\n\n/* Progress bar track (background). */\n@recipe progress-track {\n h-2 w-full overflow-hidden rounded-full bg-muted\n}\n\n/* Progress bar fill. */\n@recipe progress-bar {\n h-full rounded-full bg-primary transition-all\n}\n\n/* Indeterminate loading spinner. */\n@recipe spinner {\n inline-block h-4 w-4 animate-spin rounded-full border-2 border-border border-t-primary\n}\n",
1540
1650
  "skeleton.css": "/* shortwind: skeleton@0.0.1 sha:000000 */\n\n/* @guide\n Match the skeleton to the shape it stands in for: @skeleton (block),\n @skeleton-text (a text line), @skeleton-circle (avatar/icon). Size block and\n text skeletons with raw width/height utilities.\n*/\n\n/* Default rectangular skeleton placeholder. */\n@recipe skeleton {\n animate-pulse rounded-md bg-muted\n}\n\n/* Single-line text skeleton. */\n@recipe skeleton-text {\n h-4 w-full animate-pulse rounded bg-muted\n}\n\n/* Circular skeleton (avatar/icon). */\n@recipe skeleton-circle {\n h-10 w-10 animate-pulse rounded-full bg-muted\n}\n",
1541
- "surface.css": "/* shortwind: surface@0.0.1 sha:000000 */\n\n/* @guide\n @surface / @surface-muted / @surface-accent set a background+foreground pair\n for a region — one per section. @wrapper (or @wrapper-tight for prose)\n centers and width-caps content; there is no @wrapper-lg, set a different cap\n with max-w-* yourself. (Note: @container is reserved for Tailwind's\n container-query utility, so the content wrapper is @wrapper.) @divider-h and\n @divider-v are hairline rules.\n*/\n\n/* Default page/section surface. */\n@recipe surface {\n bg-background text-foreground\n}\n\n/* Muted surface — secondary background. */\n@recipe surface-muted {\n bg-muted text-foreground\n}\n\n/* Accent surface — soft brand background. */\n@recipe surface-accent {\n bg-accent text-accent-foreground\n}\n\n/* Centered content wrapper with a max width. */\n@recipe wrapper {\n mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8\n}\n\n/* Narrow content wrapper for prose. */\n@recipe wrapper-tight {\n mx-auto w-full max-w-3xl px-4 sm:px-6\n}\n\n/* Horizontal divider line. */\n@recipe divider-h {\n shrink-0 h-px w-full bg-border\n}\n\n/* Vertical divider line. */\n@recipe divider-v {\n shrink-0 h-full w-px bg-border\n}\n",
1651
+ "surface.css": "/* shortwind: surface@0.0.2 sha:000000 */\n\n/* @guide\n @surface / @surface-muted / @surface-accent set a background+foreground pair\n for a region — one per section. @wrapper (or @wrapper-tight for prose)\n centers and width-caps content; there is no @wrapper-lg, set a different cap\n with max-w-* yourself. (Note: @container is reserved for Tailwind's\n container-query utility, so the content wrapper is @wrapper.) @divider-h and\n @divider-v are hairline rules.\n*/\n\n/* Default page/section surface. */\n@recipe surface {\n bg-background text-foreground\n}\n\n/* Muted surface — secondary background. */\n@recipe surface-muted {\n bg-muted text-foreground\n}\n\n/* Accent surface — soft brand background. */\n@recipe surface-accent {\n bg-accent text-accent-foreground\n}\n\n/* Centered content wrapper with a max width. */\n@recipe wrapper {\n mx-auto w-full max-w-6xl px-4 sm:px-6 lg:px-8\n}\n\n/* Narrow content wrapper for prose. */\n@recipe wrapper-tight {\n mx-auto w-full max-w-3xl px-4 sm:px-6\n}\n\n/* Horizontal divider line. */\n@recipe divider-h {\n shrink-0 h-px w-full bg-border\n}\n\n/* Vertical divider line. */\n@recipe divider-v {\n shrink-0 h-full w-px bg-border\n}\n",
1542
1652
  "table.css": "/* shortwind: table@0.0.1 sha:000000 */\n\n/* @guide\n Wrap the table in @table-container for horizontal overflow, then put @table\n (or @table-zebra for striped rows) on the <table>. Cells are @th (header) and\n @td (body); add @tr-hover to a <tr> for row highlighting.\n*/\n\n/* Scroll container for a wide table — keeps overflow horizontal. */\n@recipe table-container {\n w-full overflow-x-auto rounded-md border border-border\n}\n\n/* Data table base. */\n@recipe table {\n w-full border-collapse text-left text-sm text-foreground\n}\n\n/* Table header cell. */\n@recipe th {\n border-b border-border px-3 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground\n}\n\n/* Table body cell. */\n@recipe td {\n border-b border-border px-3 py-2\n}\n\n/* Row hover state. */\n@recipe tr-hover {\n transition-colors hover:bg-muted\n}\n\n/* Table with zebra striping on alternating rows. */\n@recipe table-zebra {\n w-full border-collapse text-left text-sm text-foreground [&_tbody_tr:nth-child(odd)]:bg-muted\n}\n",
1543
- "text.css": "/* shortwind: text@0.0.1 sha:000000 */\n\n/* @guide\n Headings are sized by weight, not HTML level: @heading-xl/lg/md/sm — there\n is no @h1..@h6. Body copy: @body (default), @lead (intro paragraphs), @muted\n (secondary), @caption (fine print). Use @label for form labels and @link for\n inline links. Don't append a -text suffix: it's @body not @body-text, @muted\n not @muted-text, @link not @link-text.\n*/\n\n/* Top-level page heading. */\n@recipe heading-xl {\n text-4xl font-bold tracking-tight text-foreground\n}\n\n/* Large section heading. */\n@recipe heading-lg {\n text-2xl font-semibold tracking-tight text-foreground\n}\n\n/* Medium heading. */\n@recipe heading-md {\n text-xl font-semibold text-foreground\n}\n\n/* Small heading. */\n@recipe heading-sm {\n text-base font-semibold text-foreground\n}\n\n/* Default body text. */\n@recipe body {\n text-sm leading-6 text-foreground\n}\n\n/* Lead paragraph — larger body copy for hero/intro sections. */\n@recipe lead {\n text-lg leading-relaxed text-muted-foreground\n}\n\n/* Muted secondary text. */\n@recipe muted {\n text-sm text-muted-foreground\n}\n\n/* Form label text. */\n@recipe label {\n text-sm font-medium text-foreground\n}\n\n/* Caption — small supporting text. */\n@recipe caption {\n text-xs text-muted-foreground\n}\n\n/* Inline link with hover/focus states. */\n@recipe link {\n text-primary underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n",
1653
+ "text.css": "/* shortwind: text@0.0.2 sha:000000 */\n\n/* @guide\n Headings are sized by weight, not HTML level: @heading-xl/lg/md/sm — there\n is no @h1..@h6. Body copy: @body (default), @lead (intro paragraphs), @muted\n (secondary), @caption (fine print), @eyebrow (uppercase kicker above a\n heading). Use @label for form labels and @link for inline links. Don't\n append a -text suffix: it's @body not @body-text, @muted not @muted-text,\n @link not @link-text.\n*/\n\n/* Top-level page heading. */\n@recipe heading-xl {\n text-4xl font-bold tracking-tight text-foreground\n}\n\n/* Large section heading. */\n@recipe heading-lg {\n text-2xl font-semibold tracking-tight text-foreground\n}\n\n/* Medium heading. */\n@recipe heading-md {\n text-xl font-semibold text-foreground\n}\n\n/* Small heading. */\n@recipe heading-sm {\n text-base font-semibold text-foreground\n}\n\n/* Default body text. */\n@recipe body {\n text-sm leading-6 text-foreground\n}\n\n/* Lead paragraph — larger body copy for hero/intro sections. */\n@recipe lead {\n text-lg leading-relaxed text-muted-foreground\n}\n\n/* Muted secondary text. */\n@recipe muted {\n text-sm text-muted-foreground\n}\n\n/* Form label text. */\n@recipe label {\n text-sm font-medium text-foreground\n}\n\n/* Caption — small supporting text. */\n@recipe caption {\n text-xs text-muted-foreground\n}\n\n/* Eyebrow — uppercase kicker above a heading. */\n@recipe eyebrow {\n font-mono text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground\n}\n\n/* Inline link with hover/focus states. */\n@recipe link {\n text-primary underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\n}\n",
1544
1654
  "tooltip.css": "/* shortwind: tooltip@0.0.1 sha:000000 */\n\n/* @guide\n @tooltip is the floating label bubble — it styles appearance only, so pair it\n with your own positioning.\n*/\n\n/* Floating tooltip bubble. */\n@recipe tooltip {\n pointer-events-none z-50 rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md\n}\n"
1545
1655
  };
1546
1656
  //#endregion
@@ -1689,7 +1799,7 @@ const ALL_RULES = [
1689
1799
  "recipe/no-sibling-overlap",
1690
1800
  "recipe/reserved-name"
1691
1801
  ];
1692
- const DEFAULT_CONTENT = ["src/**/*.{html,js,jsx,ts,tsx,vue,svelte,astro,md,mdx}"];
1802
+ const DEFAULT_CONTENT = ["{src,app,pages,components,lib}/**/*.{html,js,jsx,ts,tsx,vue,svelte,astro,md,mdx}"];
1693
1803
  async function lint(options) {
1694
1804
  const cwd = path.resolve(options.cwd);
1695
1805
  const config = await readConfig(cwd);
@@ -1700,7 +1810,7 @@ async function lint(options) {
1700
1810
  findings.push(...parseFindings);
1701
1811
  findings.push(...checkRecipeNames(registry, recipesDir, enabledRules));
1702
1812
  findings.push(...checkReservedNames(registry, recipesDir, enabledRules));
1703
- const files = await glob(options.content ?? DEFAULT_CONTENT, {
1813
+ const files = await glob(options.content ?? config.content ?? DEFAULT_CONTENT, {
1704
1814
  cwd,
1705
1815
  absolute: true,
1706
1816
  onlyFiles: true,
@@ -1744,7 +1854,7 @@ async function lint(options) {
1744
1854
  }
1745
1855
  }
1746
1856
  }
1747
- if (enabledRules.has("recipe/unused")) {
1857
+ if (enabledRules.has("recipe/unused") && files.length > 0) {
1748
1858
  const recipesByName = /* @__PURE__ */ new Map();
1749
1859
  for (const recs of Object.values(registry.families)) for (const r of recs) recipesByName.set(r.name, r);
1750
1860
  for (const name of Object.keys(registry.flattened)) {
@@ -1769,7 +1879,8 @@ async function lint(options) {
1769
1879
  return {
1770
1880
  ok: !findings.some((f) => f.severity === "error"),
1771
1881
  findings,
1772
- filesFixed
1882
+ filesFixed,
1883
+ scannedFiles: files.length
1773
1884
  };
1774
1885
  }
1775
1886
  function loadRegistry(recipesDir, rules) {
@@ -2382,6 +2493,6 @@ function formatBenchTable(result) {
2382
2493
  return lines.join("\n");
2383
2494
  }
2384
2495
  //#endregion
2385
- export { buildHeaderLine as A, cliVersion as C, createRegistrySource as D, writeLockfile as E, verifyFetchedFamily as F, extractHeader as M, rewriteHeaderSha as N, resolvePresetFamilies as O, sealRecipeFile as P, DEFAULT_REGISTRY as S, readLockfile as T, NewFamilyError as _, formatFindingsText as a, add as b, UpgradeError as c, BuildError as d, build as f, reseal as g, preset as h, extractClassUsages as i, computeBodySha as j, detectProject as k, upgrade as l, ls as m, formatBenchTable as n, lint as o, formatLsText as p, ALL_RULES as r, verify as s, bench as t, dev as u, newFamily as v, init as w, renameFamilyInSource as x, remove as y };
2496
+ export { createRegistrySource as A, readConfig as C, init as D, cliVersion as E, extractHeader as F, rewriteHeaderSha as I, sealRecipeFile as L, detectProject as M, buildHeaderLine as N, readLockfile as O, computeBodySha as P, verifyFetchedFamily as R, installedFamilies as S, DEFAULT_REGISTRY as T, reseal as _, extractClassUsages as a, remove as b, verify as c, dev as d, BuildError as f, preset as g, ls as h, DEFAULT_CONTENT as i, resolvePresetFamilies as j, writeLockfile as k, UpgradeError as l, formatLsText as m, formatBenchTable as n, formatFindingsText as o, build as p, ALL_RULES as r, lint as s, bench as t, upgrade as u, NewFamilyError as v, renameFamilyInSource as w, add as x, newFamily as y };
2386
2497
 
2387
- //# sourceMappingURL=bench-oy9aPOvX.js.map
2498
+ //# sourceMappingURL=bench-B7SxNGiU.js.map